From 2ae626bcb626c27ce3c11cd364a472bcc38c1275 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 30 Jul 2026 15:22:48 +0800 Subject: [PATCH 1/6] Load feature flags from new endpoint --- .../appconfigurationprovider_controller.go | 2 + internal/controller/processor.go | 61 +++-- internal/controller/processor_test.go | 3 + internal/controller/suite_test.go | 3 + internal/loader/app_configuration_client.go | 92 +++++++ .../configuraiton_setting_loader_test.go | 56 ++-- .../loader/configuration_client_manager.go | 32 +-- .../loader/configuration_setting_loader.go | 241 ++++++++++++------ internal/loader/feature_flag_converter.go | 182 +++++++++++++ .../loader/feature_flag_converter_test.go | 201 +++++++++++++++ .../mock_configuration_settings_retriever.go | 15 ++ internal/loader/request_tracing.go | 6 + internal/loader/settings_client.go | 111 +++++++- 13 files changed, 857 insertions(+), 148 deletions(-) create mode 100644 internal/loader/app_configuration_client.go create mode 100644 internal/loader/feature_flag_converter.go create mode 100644 internal/loader/feature_flag_converter_test.go diff --git a/internal/controller/appconfigurationprovider_controller.go b/internal/controller/appconfigurationprovider_controller.go index bfe923b..1621ae2 100644 --- a/internal/controller/appconfigurationprovider_controller.go +++ b/internal/controller/appconfigurationprovider_controller.go @@ -56,6 +56,7 @@ type ReconciliationState struct { SentinelETags map[acpv1.Sentinel]*azcore.ETag KeyValueETags map[acpv1.ComparableSelector][]*azcore.ETag FeatureFlagETags map[acpv1.ComparableSelector][]*azcore.ETag + EnhancedFeatureFlagETags map[acpv1.ComparableSelector][]*azcore.ETag ExistingK8sSecrets map[string]*loader.TargetK8sSecretMetadata NextKeyValueRefreshReconcileTime metav1.Time NextSecretReferenceRefreshReconcileTime metav1.Time @@ -149,6 +150,7 @@ func (reconciler *AzureAppConfigurationProviderReconciler) Reconcile(ctx context SentinelETags: make(map[acpv1.Sentinel]*azcore.ETag), KeyValueETags: make(map[acpv1.ComparableSelector][]*azcore.ETag), FeatureFlagETags: make(map[acpv1.ComparableSelector][]*azcore.ETag), + EnhancedFeatureFlagETags: make(map[acpv1.ComparableSelector][]*azcore.ETag), ExistingK8sSecrets: make(map[string]*loader.TargetK8sSecretMetadata), ClientManager: nil, } diff --git a/internal/controller/processor.go b/internal/controller/processor.go index faef321..bc5cca5 100644 --- a/internal/controller/processor.go +++ b/internal/controller/processor.go @@ -30,18 +30,20 @@ type AppConfigurationProviderProcessor struct { } type RefreshOptions struct { - keyValueRefreshEnabled bool - secretReferenceRefreshEnabled bool - secretReferenceRefreshNeeded bool - featureFlagRefreshEnabled bool - featureFlagRefreshNeeded bool - ConfigMapSettingPopulated bool - SecretSettingPopulated bool - sentinelChanged bool - keyValuePageETagsChanged bool - updatedSentinelETags map[acpv1.Sentinel]*azcore.ETag - updatedKeyValueETags map[acpv1.ComparableSelector][]*azcore.ETag - updatedFeatureFlagETags map[acpv1.ComparableSelector][]*azcore.ETag + keyValueRefreshEnabled bool + secretReferenceRefreshEnabled bool + secretReferenceRefreshNeeded bool + featureFlagRefreshEnabled bool + featureFlagRefreshNeeded bool + enhancedFeatureFlagRefreshNeeded bool + ConfigMapSettingPopulated bool + SecretSettingPopulated bool + sentinelChanged bool + keyValuePageETagsChanged bool + updatedSentinelETags map[acpv1.Sentinel]*azcore.ETag + updatedKeyValueETags map[acpv1.ComparableSelector][]*azcore.ETag + updatedFeatureFlagETags map[acpv1.ComparableSelector][]*azcore.ETag + updatedEnhancedFeatureFlagETags map[acpv1.ComparableSelector][]*azcore.ETag } func (processor *AppConfigurationProviderProcessor) PopulateSettings(existingConfigMap *corev1.ConfigMap, existingSecrets map[string]corev1.Secret) error { @@ -75,6 +77,7 @@ func (processor *AppConfigurationProviderProcessor) processFullReconciliation() processor.RefreshOptions.ConfigMapSettingPopulated = true processor.RefreshOptions.updatedKeyValueETags = updatedSettings.KeyValueETags processor.RefreshOptions.updatedFeatureFlagETags = updatedSettings.FeatureFlagETags + processor.RefreshOptions.updatedEnhancedFeatureFlagETags = updatedSettings.EnhancedFeatureFlagETags processor.RefreshOptions.updatedSentinelETags = updatedSettings.SentinelETags if processor.Provider.Spec.Secret != nil { processor.RefreshOptions.SecretSettingPopulated = true @@ -112,7 +115,11 @@ func (processor *AppConfigurationProviderProcessor) processFeatureFlagRefresh(ex return err } - if !processor.RefreshOptions.featureFlagRefreshNeeded { + if processor.RefreshOptions.enhancedFeatureFlagRefreshNeeded, err = (processor.Retriever).CheckIfEnhancedFeatureFlagsChanged(processor.Context, reconcileState.EnhancedFeatureFlagETags); err != nil { + return err + } + + if !(processor.RefreshOptions.featureFlagRefreshNeeded || processor.RefreshOptions.enhancedFeatureFlagRefreshNeeded) { reconcileState.NextFeatureFlagRefreshReconcileTime = nextFeatureFlagRefreshReconcileTime return nil } @@ -123,6 +130,7 @@ func (processor *AppConfigurationProviderProcessor) processFeatureFlagRefresh(ex } processor.RefreshOptions.updatedFeatureFlagETags = featureFlagRefreshedSettings.FeatureFlagETags + processor.RefreshOptions.updatedEnhancedFeatureFlagETags = featureFlagRefreshedSettings.EnhancedFeatureFlagETags processor.Settings = featureFlagRefreshedSettings processor.RefreshOptions.ConfigMapSettingPopulated = true // Update next refresh time only if settings updated successfully @@ -323,6 +331,10 @@ func (processor *AppConfigurationProviderProcessor) Finish() (ctrl.Result, error processor.ReconciliationState.FeatureFlagETags = processor.RefreshOptions.updatedFeatureFlagETags } + if processor.RefreshOptions.updatedEnhancedFeatureFlagETags != nil { + processor.ReconciliationState.EnhancedFeatureFlagETags = processor.RefreshOptions.updatedEnhancedFeatureFlagETags + } + if processor.ShouldReconcile { processor.ReconciliationState.SentinelETags = processor.RefreshOptions.updatedSentinelETags } @@ -348,7 +360,7 @@ func (processor *AppConfigurationProviderProcessor) Finish() (ctrl.Result, error processor.Provider.Status.RefreshStatus.LastKeyVaultReferenceRefreshTime = processor.CurrentTime } // Update provider last feature flag refresh time - if processor.RefreshOptions.featureFlagRefreshNeeded { + if processor.RefreshOptions.featureFlagRefreshNeeded || processor.RefreshOptions.enhancedFeatureFlagRefreshNeeded { processor.Provider.Status.RefreshStatus.LastFeatureFlagRefreshTime = processor.CurrentTime } // At least one dynamic feature is enabled, requeueAfterInterval need be recalculated @@ -361,16 +373,17 @@ func (processor *AppConfigurationProviderProcessor) Finish() (ctrl.Result, error func NewRefreshOptions() *RefreshOptions { return &RefreshOptions{ - keyValueRefreshEnabled: false, - secretReferenceRefreshEnabled: false, - secretReferenceRefreshNeeded: false, - featureFlagRefreshEnabled: false, - featureFlagRefreshNeeded: false, - ConfigMapSettingPopulated: false, - SecretSettingPopulated: false, - sentinelChanged: false, - keyValuePageETagsChanged: false, - updatedSentinelETags: make(map[acpv1.Sentinel]*azcore.ETag), + keyValueRefreshEnabled: false, + secretReferenceRefreshEnabled: false, + secretReferenceRefreshNeeded: false, + featureFlagRefreshEnabled: false, + featureFlagRefreshNeeded: false, + enhancedFeatureFlagRefreshNeeded: false, + ConfigMapSettingPopulated: false, + SecretSettingPopulated: false, + sentinelChanged: false, + keyValuePageETagsChanged: false, + updatedSentinelETags: make(map[acpv1.Sentinel]*azcore.ETag), } } diff --git a/internal/controller/processor_test.go b/internal/controller/processor_test.go index 173f590..1931393 100644 --- a/internal/controller/processor_test.go +++ b/internal/controller/processor_test.go @@ -36,6 +36,9 @@ var _ = Describe("AppConfiguationProvider processor", func() { BeforeEach(func() { mockCtrl = gomock.NewController(GinkgoT()) mockConfigurationSettings = mocks.NewMockConfigurationSettingsRetriever(mockCtrl) + // The dedicated feature flag endpoint is checked whenever the classic feature flag page ETags + // are unchanged; default to reporting no change so existing scenarios are unaffected. + mockConfigurationSettings.EXPECT().CheckIfEnhancedFeatureFlagsChanged(gomock.Any(), gomock.Any()).Return(false, nil).AnyTimes() }) AfterEach(func() { diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 9abde0b..84c2cfb 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -88,6 +88,9 @@ var _ = BeforeSuite(func() { mockCtrl = gomock.NewController(GinkgoT()) mockConfigurationSettings = mocks.NewMockConfigurationSettingsRetriever(mockCtrl) + // The dedicated feature flag endpoint is checked whenever the classic feature flag page ETags + // are unchanged; default to reporting no change so existing scenarios are unaffected. + mockConfigurationSettings.EXPECT().CheckIfEnhancedFeatureFlagsChanged(gomock.Any(), gomock.Any()).Return(false, nil).AnyTimes() err = (&AzureAppConfigurationProviderReconciler{ Client: k8sManager.GetClient(), diff --git a/internal/loader/app_configuration_client.go b/internal/loader/app_configuration_client.go new file mode 100644 index 0000000..38703ea --- /dev/null +++ b/internal/loader/app_configuration_client.go @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package loader + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + azappconfig "github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2" +) + +// AppConfigurationClient abstracts the Azure App Configuration operations used by the provider. +type AppConfigurationClient interface { + // Key-value configuration operations. + NewListSettingsPager(selector azappconfig.SettingSelector, options *azappconfig.ListSettingsOptions) *runtime.Pager[azappconfig.ListSettingsPageResponse] + GetSetting(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) + GetSnapshot(ctx context.Context, snapshotName string, options *azappconfig.GetSnapshotOptions) (azappconfig.GetSnapshotResponse, error) + NewListSettingsForSnapshotPager(snapshotName string, options *azappconfig.ListSettingsForSnapshotOptions) *runtime.Pager[azappconfig.ListSettingsForSnapshotResponse] + + // Feature flag operations served by the dedicated feature flag endpoint. + NewListFeatureFlagsPager(selector azappconfig.FeatureFlagSelector, options *azappconfig.ListFeatureFlagsOptions) *runtime.Pager[azappconfig.ListFeatureFlagsPageResponse] +} + +type appConfigurationClient struct { + configurationClient *azappconfig.Client + featureFlagClient *azappconfig.FeatureFlagClient +} + +func NewAppConfigurationClient(endpoint string, credential azcore.TokenCredential, options *azappconfig.ClientOptions) (AppConfigurationClient, error) { + configurationClient, err := azappconfig.NewClient(endpoint, credential, options) + if err != nil { + return nil, err + } + + featureFlagClient, err := azappconfig.NewFeatureFlagClient(endpoint, credential, featureFlagClientOptions(options)) + if err != nil { + return nil, err + } + + return &appConfigurationClient{ + configurationClient: configurationClient, + featureFlagClient: featureFlagClient, + }, nil +} + +func NewAppConfigurationClientFromConnectionString(connectionString string, options *azappconfig.ClientOptions) (AppConfigurationClient, error) { + configurationClient, err := azappconfig.NewClientFromConnectionString(connectionString, options) + if err != nil { + return nil, err + } + + featureFlagClient, err := azappconfig.NewFeatureFlagClientFromConnectionString(connectionString, featureFlagClientOptions(options)) + if err != nil { + return nil, err + } + + return &appConfigurationClient{ + configurationClient: configurationClient, + featureFlagClient: featureFlagClient, + }, nil +} + +// featureFlagClientOptions mirrors the configuration client options onto feature flag client options +func featureFlagClientOptions(options *azappconfig.ClientOptions) *azappconfig.FeatureFlagClientOptions { + if options == nil { + return nil + } + + return &azappconfig.FeatureFlagClientOptions{ClientOptions: options.ClientOptions} +} + +func (c *appConfigurationClient) NewListSettingsPager(selector azappconfig.SettingSelector, options *azappconfig.ListSettingsOptions) *runtime.Pager[azappconfig.ListSettingsPageResponse] { + return c.configurationClient.NewListSettingsPager(selector, options) +} + +func (c *appConfigurationClient) GetSetting(ctx context.Context, key string, options *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) { + return c.configurationClient.GetSetting(ctx, key, options) +} + +func (c *appConfigurationClient) GetSnapshot(ctx context.Context, snapshotName string, options *azappconfig.GetSnapshotOptions) (azappconfig.GetSnapshotResponse, error) { + return c.configurationClient.GetSnapshot(ctx, snapshotName, options) +} + +func (c *appConfigurationClient) NewListSettingsForSnapshotPager(snapshotName string, options *azappconfig.ListSettingsForSnapshotOptions) *runtime.Pager[azappconfig.ListSettingsForSnapshotResponse] { + return c.configurationClient.NewListSettingsForSnapshotPager(snapshotName, options) +} + +func (c *appConfigurationClient) NewListFeatureFlagsPager(selector azappconfig.FeatureFlagSelector, options *azappconfig.ListFeatureFlagsOptions) *runtime.Pager[azappconfig.ListFeatureFlagsPageResponse] { + return c.featureFlagClient.NewListFeatureFlagsPager(selector, options) +} diff --git a/internal/loader/configuraiton_setting_loader_test.go b/internal/loader/configuraiton_setting_loader_test.go index 9b00ace..d0cb491 100644 --- a/internal/loader/configuraiton_setting_loader_test.go +++ b/internal/loader/configuraiton_setting_loader_test.go @@ -41,7 +41,7 @@ var ( mockCtrl *gomock.Controller mockCongiurationClientManager *MockClientManager endpointName string = "https://fake-endpoint" - fakeClientWrapper = ConfigurationClientWrapper{ + fakeClientWrapper = AppConfigurationClientWrapper{ Client: nil, Endpoint: endpointName, BackOffEndTime: metav1.Time{}, @@ -244,10 +244,10 @@ func (m *MockClientManager) EXPECT() *MockClientManagerMockRecorder { } // GetClients mocks base method. -func (m *MockClientManager) GetClients(arg0 context.Context) ([]*ConfigurationClientWrapper, error) { +func (m *MockClientManager) GetClients(arg0 context.Context) ([]*AppConfigurationClientWrapper, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetClients", arg0) - ret0, _ := ret[0].([]*ConfigurationClientWrapper) + ret0, _ := ret[0].([]*AppConfigurationClientWrapper) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -294,7 +294,7 @@ func (m *MockSettingsClient) EXPECT() *MockSettingsClientMockRecorder { } // GetSettings mocks base method. -func (m *MockSettingsClient) GetSettings(arg0 context.Context, arg1 *azappconfig.Client) (*SettingsResponse, error) { +func (m *MockSettingsClient) GetSettings(arg0 context.Context, arg1 AppConfigurationClient) (*SettingsResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSettings", arg0, arg1) ret0, _ := ret[0].(*SettingsResponse) @@ -377,7 +377,7 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() { Spec: testSpec, } - mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil) + mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&fakeClientWrapper}, nil) configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient) secretValue := "fakeSecretValue" secret1 := azsecrets.GetSecretResponse{ @@ -444,7 +444,7 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() { Spec: testSpec, } - mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil) + mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&fakeClientWrapper}, nil) configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient) secretValue := "fakeSecretValue" secret1 := azsecrets.GetSecretResponse{ @@ -513,7 +513,7 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() { Spec: testSpec, } - mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil) + mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&fakeClientWrapper}, nil) configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient) secretValue := "fakeSecretValue" secret1 := azsecrets.GetSecretResponse{ @@ -573,7 +573,7 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() { Etags: keyValueEtags, } mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil) - mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil) + mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&fakeClientWrapper}, nil) configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient) allSettings, err := configurationProvider.CreateTargetSettings(context.Background(), mockResolveSecretReference) @@ -1064,7 +1064,7 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() { Etags: keyValueEtags, } mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil) - mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil) + mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&fakeClientWrapper}, nil) configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient) allSettings, err := configurationProvider.CreateTargetSettings(context.Background(), mockResolveSecretReference) @@ -1112,7 +1112,7 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() { Etags: keyValueEtags, } mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil) - mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil) + mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&fakeClientWrapper}, nil) configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient) allSettings, err := configurationProvider.CreateTargetSettings(context.Background(), mockResolveSecretReference) @@ -1154,7 +1154,7 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() { Etags: keyValueEtags, } mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil) - mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil) + mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&fakeClientWrapper}, nil) configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient) allSettings, err := configurationProvider.CreateTargetSettings(context.Background(), mockResolveSecretReference) @@ -1198,7 +1198,7 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() { Etags: keyValueEtags, } mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil) - mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil) + mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&fakeClientWrapper}, nil) configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient) allSettings, err := configurationProvider.CreateTargetSettings(context.Background(), mockResolveSecretReference) @@ -1250,8 +1250,8 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() { Settings: featureFlagsToReturn, Etags: featureFlagEtags, } - mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil).Times(2) - mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil).Times(2) + mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil).Times(3) + mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&fakeClientWrapper}, nil).Times(3) configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient) allSettings, err := configurationProvider.CreateTargetSettings(context.Background(), mockResolveSecretReference) @@ -1300,8 +1300,8 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() { Settings: featureFlagsToReturn, Etags: featureFlagEtags, } - mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil).Times(2) - mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil).Times(2) + mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil).Times(3) + mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&fakeClientWrapper}, nil).Times(3) configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient) allSettings, err := configurationProvider.CreateTargetSettings(context.Background(), mockResolveSecretReference) @@ -1351,8 +1351,8 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() { Settings: featureFlagsToReturn, Etags: featureFlagEtags, } - mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil).Times(2) - mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil).Times(2) + mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil).Times(3) + mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&fakeClientWrapper}, nil).Times(3) configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient) allSettings, err := configurationProvider.CreateTargetSettings(context.Background(), mockResolveSecretReference) @@ -1385,7 +1385,7 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() { err := errors.New("fake error") mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(nil, err) - mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil) + mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&fakeClientWrapper}, nil) configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient) allSettings, err := configurationProvider.CreateTargetSettings(context.Background(), mockResolveSecretReference) @@ -1418,14 +1418,14 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() { netErr := &net.OpError{Err: errors.New("fake network error")} settingsToReturn := mockConfigurationSettings() - failedClient := ConfigurationClientWrapper{ + failedClient := AppConfigurationClientWrapper{ Client: nil, Endpoint: endpointName, BackOffEndTime: metav1.Time{}, FailedAttempts: 0, } - succeededClient := ConfigurationClientWrapper{ + succeededClient := AppConfigurationClientWrapper{ Client: nil, Endpoint: endpointName, BackOffEndTime: metav1.Time{}, @@ -1440,7 +1440,7 @@ var _ = Describe("AppConfiguationProvider Get All Settings", func() { } mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(nil, netErr).Times(1) mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil).Times(1) - mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&failedClient, &succeededClient}, nil) + mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&failedClient, &succeededClient}, nil) configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient) allSettings, err := configurationProvider.CreateTargetSettings(context.Background(), mockResolveSecretReference) @@ -1499,7 +1499,7 @@ var _ = Describe("TagFilters Support", func() { Spec: testSpec, } - mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil) + mockCongiurationClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&fakeClientWrapper}, nil) configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockCongiurationClientManager, mockSettingsClient) settingsToReturn := mockConfigurationSettings() @@ -2301,8 +2301,8 @@ func TestSnapshotReferenceInCreateKeyValueSettings(t *testing.T) { // First GetClients call: for ExecuteFailoverPolicy (initial key-value loading) - returns valid wrapper // Second GetClients call: for resolveSnapshotReferences - returns empty to trigger error gomock.InOrder( - mockClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil), - mockClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{}, nil), + mockClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&fakeClientWrapper}, nil), + mockClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{}, nil), ) configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockClientManager, mockSettingsClient) @@ -2354,7 +2354,7 @@ func TestSnapshotReferenceInCreateKeyValueSettings(t *testing.T) { } mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil) - mockClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{&fakeClientWrapper}, nil).AnyTimes() + mockClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{&fakeClientWrapper}, nil).AnyTimes() configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockClientManager, mockSettingsClient) rawSettings, err := configurationProvider.CreateKeyValueSettings(context.Background(), nil) @@ -2413,11 +2413,11 @@ func TestSnapshotReferenceInCreateKeyValueSettings(t *testing.T) { } mockSettingsClient.EXPECT().GetSettings(gomock.Any(), gomock.Any()).Return(settingsResponse, nil) - fakeClient := &ConfigurationClientWrapper{ + fakeClient := &AppConfigurationClientWrapper{ Client: nil, Endpoint: EndpointName, } - mockClientManager.EXPECT().GetClients(gomock.Any()).Return([]*ConfigurationClientWrapper{fakeClient}, nil).AnyTimes() + mockClientManager.EXPECT().GetClients(gomock.Any()).Return([]*AppConfigurationClientWrapper{fakeClient}, nil).AnyTimes() configurationProvider, _ := NewConfigurationSettingLoader(testProvider, mockClientManager, mockSettingsClient) _, err := configurationProvider.CreateKeyValueSettings(context.Background(), nil) diff --git a/internal/loader/configuration_client_manager.go b/internal/loader/configuration_client_manager.go index ddc05f4..fa7cc98 100644 --- a/internal/loader/configuration_client_manager.go +++ b/internal/loader/configuration_client_manager.go @@ -39,8 +39,8 @@ import ( type ConfigurationClientManager struct { ReplicaDiscoveryEnabled bool LoadBalancingEnabled bool - StaticClientWrappers []*ConfigurationClientWrapper - DynamicClientWrappers []*ConfigurationClientWrapper + StaticClientWrappers []*AppConfigurationClientWrapper + DynamicClientWrappers []*AppConfigurationClientWrapper validDomain string endpoint string credential azcore.TokenCredential @@ -51,15 +51,15 @@ type ConfigurationClientManager struct { lastSuccessfulEndpoint string } -type ConfigurationClientWrapper struct { +type AppConfigurationClientWrapper struct { Endpoint string - Client *azappconfig.Client + Client AppConfigurationClient BackOffEndTime metav1.Time FailedAttempts int } type ClientManager interface { - GetClients(ctx context.Context) ([]*ConfigurationClientWrapper, error) + GetClients(ctx context.Context) ([]*AppConfigurationClientWrapper, error) RefreshClients(ctx context.Context) } @@ -114,7 +114,7 @@ func NewConfigurationClientManager(ctx context.Context, provider acpv1.AzureAppC } var err error - var staticClient *azappconfig.Client + var staticClient AppConfigurationClient if provider.Spec.ConnectionStringReference != nil { connectionString, err := getConnectionStringParameter(ctx, types.NamespacedName{Namespace: provider.Namespace, Name: *provider.Spec.ConnectionStringReference}) if err != nil { @@ -132,21 +132,21 @@ func NewConfigurationClientManager(ctx context.Context, provider acpv1.AzureAppC if manager.id, err = parseConnectionString(connectionString, IdSection); err != nil { return nil, err } - if staticClient, err = azappconfig.NewClientFromConnectionString(connectionString, newClientOptions()); err != nil { + if staticClient, err = NewAppConfigurationClientFromConnectionString(connectionString, newClientOptions()); err != nil { return nil, err } } else { if manager.credential, err = CreateTokenCredential(ctx, provider.Spec.Auth, provider.Namespace); err != nil { return nil, err } - if staticClient, err = azappconfig.NewClient(*provider.Spec.Endpoint, manager.credential, newClientOptions()); err != nil { + if staticClient, err = NewAppConfigurationClient(*provider.Spec.Endpoint, manager.credential, newClientOptions()); err != nil { return nil, err } manager.endpoint = *provider.Spec.Endpoint } manager.validDomain = getValidDomain(manager.endpoint) - manager.StaticClientWrappers = []*ConfigurationClientWrapper{{ + manager.StaticClientWrappers = []*AppConfigurationClientWrapper{{ Endpoint: manager.endpoint, Client: staticClient, BackOffEndTime: metav1.Time{}, @@ -156,9 +156,9 @@ func NewConfigurationClientManager(ctx context.Context, provider acpv1.AzureAppC return manager, nil } -func (manager *ConfigurationClientManager) GetClients(ctx context.Context) ([]*ConfigurationClientWrapper, error) { +func (manager *ConfigurationClientManager) GetClients(ctx context.Context) ([]*AppConfigurationClientWrapper, error) { currentTime := metav1.Now() - clients := make([]*ConfigurationClientWrapper, 0) + clients := make([]*AppConfigurationClientWrapper, 0) for _, clientWrapper := range manager.StaticClientWrappers { if currentTime.After(clientWrapper.BackOffEndTime.Time) { clients = append(clients, clientWrapper) @@ -227,7 +227,7 @@ func (manager *ConfigurationClientManager) DiscoverFallbackClients(ctx context.C srvTargetHosts[i], srvTargetHosts[j] = srvTargetHosts[j], srvTargetHosts[i] } - newDynamicClients := make([]*ConfigurationClientWrapper, 0) + newDynamicClients := make([]*AppConfigurationClientWrapper, 0) for _, host := range srvTargetHosts { if isValidEndpoint(host, manager.validDomain) { targetEndpoint := "https://" + host @@ -239,7 +239,7 @@ func (manager *ConfigurationClientManager) DiscoverFallbackClients(ctx context.C klog.Warningf("build fallback clients failed, %s", err.Error()) return } - newDynamicClients = append(newDynamicClients, &ConfigurationClientWrapper{ + newDynamicClients = append(newDynamicClients, &AppConfigurationClientWrapper{ Endpoint: targetEndpoint, Client: client, BackOffEndTime: metav1.Time{}, @@ -298,9 +298,9 @@ func QuerySrvTargetHost(ctx context.Context, host string) ([]string, error) { return results, nil } -func (manager *ConfigurationClientManager) newConfigurationClient(endpoint string) (*azappconfig.Client, error) { +func (manager *ConfigurationClientManager) newConfigurationClient(endpoint string) (AppConfigurationClient, error) { if manager.credential != nil { - return azappconfig.NewClient(endpoint, manager.credential, newClientOptions()) + return NewAppConfigurationClient(endpoint, manager.credential, newClientOptions()) } connectionStr := buildConnectionString(endpoint, manager.secret, manager.id) @@ -308,7 +308,7 @@ func (manager *ConfigurationClientManager) newConfigurationClient(endpoint strin return nil, fmt.Errorf("failed to build connection string for fallback client") } - return azappconfig.NewClientFromConnectionString(connectionStr, newClientOptions()) + return NewAppConfigurationClientFromConnectionString(connectionStr, newClientOptions()) } func isValidEndpoint(host string, validDomain string) bool { diff --git a/internal/loader/configuration_setting_loader.go b/internal/loader/configuration_setting_loader.go index d43cee1..411f3ba 100644 --- a/internal/loader/configuration_setting_loader.go +++ b/internal/loader/configuration_setting_loader.go @@ -46,11 +46,12 @@ type ConfigurationSettingLoader struct { type TargetKeyValueSettings struct { ConfigMapSettings map[string]string // Multiple secrets could be managed - SecretSettings map[string]corev1.Secret - K8sSecrets map[string]*TargetK8sSecretMetadata - KeyValueETags map[acpv1.ComparableSelector][]*azcore.ETag - FeatureFlagETags map[acpv1.ComparableSelector][]*azcore.ETag - SentinelETags map[acpv1.Sentinel]*azcore.ETag + SecretSettings map[string]corev1.Secret + K8sSecrets map[string]*TargetK8sSecretMetadata + KeyValueETags map[acpv1.ComparableSelector][]*azcore.ETag + SentinelETags map[acpv1.Sentinel]*azcore.ETag + FeatureFlagETags map[acpv1.ComparableSelector][]*azcore.ETag + EnhancedFeatureFlagETags map[acpv1.ComparableSelector][]*azcore.ETag } type TargetK8sSecretMetadata struct { @@ -60,19 +61,21 @@ type TargetK8sSecretMetadata struct { } type RawSettings struct { - KeyValueSettings map[string]*string - IsJsonContentTypeMap map[string]bool - FeatureFlagSettings map[string]interface{} - SecretSettings map[string]corev1.Secret - K8sSecrets map[string]*TargetK8sSecretMetadata - KeyValueETags map[acpv1.ComparableSelector][]*azcore.ETag - FeatureFlagETags map[acpv1.ComparableSelector][]*azcore.ETag + KeyValueSettings map[string]*string + IsJsonContentTypeMap map[string]bool + FeatureFlagSettings map[string]interface{} + SecretSettings map[string]corev1.Secret + K8sSecrets map[string]*TargetK8sSecretMetadata + KeyValueETags map[acpv1.ComparableSelector][]*azcore.ETag + FeatureFlagETags map[acpv1.ComparableSelector][]*azcore.ETag + EnhancedFeatureFlagETags map[acpv1.ComparableSelector][]*azcore.ETag } type ConfigurationSettingsRetriever interface { CreateTargetSettings(ctx context.Context, resolveSecretReference SecretReferenceResolver) (*TargetKeyValueSettings, error) CheckAndRefreshSentinels(ctx context.Context, provider *acpv1.AzureAppConfigurationProvider, eTags map[acpv1.Sentinel]*azcore.ETag) (bool, map[acpv1.Sentinel]*azcore.ETag, error) CheckPageETags(ctx context.Context, eTags map[acpv1.ComparableSelector][]*azcore.ETag) (bool, error) + CheckIfEnhancedFeatureFlagsChanged(ctx context.Context, eTags map[acpv1.ComparableSelector][]*azcore.ETag) (bool, error) RefreshKeyValueSettings(ctx context.Context, existingConfigMapSettings *map[string]string, resolveSecretReference SecretReferenceResolver) (*TargetKeyValueSettings, error) RefreshFeatureFlagSettings(ctx context.Context, existingConfigMapSettings *map[string]string) (*TargetKeyValueSettings, error) ResolveSecretReferences(ctx context.Context, kvReferencesToResolve map[string]*TargetK8sSecretMetadata, kvResolver SecretReferenceResolver) (*TargetKeyValueSettings, error) @@ -95,6 +98,7 @@ const ( FeatureFlagKeyPrefix string = ".appconfig.featureflag/" FeatureFlagSectionName string = "feature_flags" FeatureManagementSectionName string = "feature_management" + FeatureFlagIdKey string = "id" PreservedSecretTypeTag string = ".kubernetes.secret.type" CertTypePem string = "application/x-pem-file" CertTypePfx string = "application/x-pkcs12" @@ -133,9 +137,24 @@ func (csl *ConfigurationSettingLoader) CreateTargetSettings(ctx context.Context, } if csl.Spec.FeatureFlag != nil { - if rawSettings.FeatureFlagSettings, rawSettings.FeatureFlagETags, err = csl.getFeatureFlagSettings(ctx); err != nil { + featureFlags, featureFlagETags, err := csl.loadFeatureFlags(ctx) + if err != nil { + return nil, err + } + + enhancedFeatureFlags, enhancedFeatureFlagETags, err := csl.loadEnhancedFeatureFlags(ctx) + if err != nil { return nil, err } + + deduplicatedFeatureFlags, err := csl.ProcessFeatureFlags(featureFlags, enhancedFeatureFlags) + if err != nil { + return nil, err + } + + rawSettings.FeatureFlagETags = featureFlagETags + rawSettings.EnhancedFeatureFlagETags = enhancedFeatureFlagETags + rawSettings.FeatureFlagSettings = deduplicatedFeatureFlags } typedSettings, err := createTypedSettings(rawSettings, csl.Spec.Target.ConfigMapData) @@ -144,12 +163,13 @@ func (csl *ConfigurationSettingLoader) CreateTargetSettings(ctx context.Context, } return &TargetKeyValueSettings{ - ConfigMapSettings: typedSettings, - SecretSettings: rawSettings.SecretSettings, - K8sSecrets: rawSettings.K8sSecrets, - KeyValueETags: rawSettings.KeyValueETags, - FeatureFlagETags: rawSettings.FeatureFlagETags, - SentinelETags: initializedSentinelETags, + ConfigMapSettings: typedSettings, + SecretSettings: rawSettings.SecretSettings, + K8sSecrets: rawSettings.K8sSecrets, + KeyValueETags: rawSettings.KeyValueETags, + EnhancedFeatureFlagETags: rawSettings.EnhancedFeatureFlagETags, + FeatureFlagETags: rawSettings.FeatureFlagETags, + SentinelETags: initializedSentinelETags, }, nil } @@ -180,7 +200,17 @@ func (csl *ConfigurationSettingLoader) RefreshKeyValueSettings(ctx context.Conte } func (csl *ConfigurationSettingLoader) RefreshFeatureFlagSettings(ctx context.Context, existingConfigMapSetting *map[string]string) (*TargetKeyValueSettings, error) { - latestFeatureFlagSettings, latestFeatureFlagETags, err := csl.getFeatureFlagSettings(ctx) + featureFlags, featureFlagETags, err := csl.loadFeatureFlags(ctx) + if err != nil { + return nil, err + } + + enhancedFeatureFlags, enhancedFeatureFlagETags, err := csl.loadEnhancedFeatureFlags(ctx) + if err != nil { + return nil, err + } + + latestFeatureFlags, err := csl.ProcessFeatureFlags(featureFlags, enhancedFeatureFlags) if err != nil { return nil, err } @@ -190,7 +220,7 @@ func (csl *ConfigurationSettingLoader) RefreshFeatureFlagSettings(ctx context.Co return nil, err } - existingSettings[FeatureManagementSectionName] = latestFeatureFlagSettings + existingSettings[FeatureManagementSectionName] = latestFeatureFlags typedStr, err := marshalJsonYaml(existingSettings, csl.Spec.Target.ConfigMapData) if err != nil { return nil, err @@ -200,7 +230,8 @@ func (csl *ConfigurationSettingLoader) RefreshFeatureFlagSettings(ctx context.Co ConfigMapSettings: map[string]string{ csl.Spec.Target.ConfigMapData.Key: typedStr, }, - FeatureFlagETags: latestFeatureFlagETags, + EnhancedFeatureFlagETags: enhancedFeatureFlagETags, + FeatureFlagETags: featureFlagETags, }, nil } @@ -208,8 +239,8 @@ func (csl *ConfigurationSettingLoader) RefreshFeatureFlagSettings(ctx context.Co type settingProcessContext struct { rawSettings *RawSettings resolver *SecretReferenceResolver - allowSnapshotRef bool // false inside a snapshot's resolved settings to prevent nested resolution - snapshotClient *azappconfig.Client // lazily initialized when the first snapshot reference is resolved + allowSnapshotRef bool // false inside a snapshot's resolved settings to prevent nested resolution + snapshotClient AppConfigurationClient // lazily initialized when the first snapshot reference is resolved useAIConfiguration bool useAIChatCompletionConfiguration bool } @@ -383,8 +414,6 @@ func (csl *ConfigurationSettingLoader) processSettings(ctx context.Context, sett if err := csl.processSettings(ctx, snapshotSettings, nestedCtx); err != nil { return err } - processCtx.useAIConfiguration = processCtx.useAIConfiguration || nestedCtx.useAIConfiguration - processCtx.useAIChatCompletionConfiguration = processCtx.useAIChatCompletionConfiguration || nestedCtx.useAIChatCompletionConfiguration default: processCtx.rawSettings.KeyValueSettings[trimmedKey] = setting.Value processCtx.rawSettings.IsJsonContentTypeMap[trimmedKey] = isJsonContentType(setting.ContentType) @@ -468,7 +497,24 @@ func (csl *ConfigurationSettingLoader) CheckPageETags(ctx context.Context, eTags return settingsResponse.Etags != nil, nil } -func (csl *ConfigurationSettingLoader) getFeatureFlagSettings(ctx context.Context) (map[string]interface{}, map[acpv1.ComparableSelector][]*azcore.ETag, error) { +func (csl *ConfigurationSettingLoader) CheckIfEnhancedFeatureFlagsChanged(ctx context.Context, eTags map[acpv1.ComparableSelector][]*azcore.ETag) (bool, error) { + settingsClient := csl.SettingsClient + if settingsClient == nil { + settingsClient = &EnhancedFeatureFlagEtagsClient{ + etags: eTags, + } + } + + settingsResponse, err := csl.ExecuteFailoverPolicy(ctx, settingsClient) + if err != nil { + return false, err + } + + // a non-nil Etags map signals that the feature flag endpoint page ETags changed + return settingsResponse.Etags != nil, nil +} + +func (csl *ConfigurationSettingLoader) loadFeatureFlags(ctx context.Context) ([]azappconfig.Setting, map[acpv1.ComparableSelector][]*azcore.ETag, error) { featureFlagFilters := GetFeatureFlagFilters(csl.Spec) settingsClient := csl.SettingsClient if settingsClient == nil { @@ -476,47 +522,101 @@ func (csl *ConfigurationSettingLoader) getFeatureFlagSettings(ctx context.Contex selectors: featureFlagFilters, } } + + settingsResponse, err := csl.ExecuteFailoverPolicy(ctx, settingsClient) + if err != nil { + return nil, nil, err + } + + return settingsResponse.Settings, settingsResponse.Etags, nil +} + +func (csl *ConfigurationSettingLoader) loadEnhancedFeatureFlags(ctx context.Context) ([]azappconfig.FeatureFlag, map[acpv1.ComparableSelector][]*azcore.ETag, error) { + settingsClient := csl.SettingsClient + if settingsClient == nil { + settingsClient = &EnhancedFeatureFlagSettingsClient{ + enhancedFeatureFlagSelectors: GetEnhancedFeatureFlagFilters(csl.Spec), + } + } + settingsResponse, err := csl.ExecuteFailoverPolicy(ctx, settingsClient) if err != nil { return nil, nil, err } - settingsLength := len(settingsResponse.Settings) - featureFlagExist := make(map[string]bool, settingsLength) - deduplicatedFeatureFlags := make([]interface{}, 0) + csl.TracingFeatures.UseEnhancedFeatureFlag = len(settingsResponse.EnhancedFeatureFlags) > 0 + return settingsResponse.EnhancedFeatureFlags, settingsResponse.Etags, nil +} + +func (csl *ConfigurationSettingLoader) ProcessFeatureFlags(featureFlags []azappconfig.Setting, enhancedFeatureFlags []azappconfig.FeatureFlag) (map[string]interface{}, error) { clientEndpoint := "" if manager, ok := csl.ClientManager.(*ConfigurationClientManager); ok { // use primary client endpoint in feature flag reference clientEndpoint = manager.StaticClientWrappers[0].Endpoint } - // if settings returned like this: [{"id": "Beta"...}, {"id": "Alpha"...}, {"id": "Beta"...}], we need to deduplicate it to [{"id": "Alpha"...}, {"id": "Beta"...}], the last one wins - for i := settingsLength - 1; i >= 0; i-- { - key := *settingsResponse.Settings[i].Key - if featureFlagExist[key] { + mergedFeatureFlags := make([]map[string]interface{}, 0, len(featureFlags)+len(enhancedFeatureFlags)) + for _, setting := range featureFlags { + if setting.Key == nil || setting.Value == nil { continue } - featureFlagExist[key] = true - var out map[string]interface{} - err := json.Unmarshal([]byte(*settingsResponse.Settings[i].Value), &out) - if err != nil { - return nil, nil, fmt.Errorf("failed to unmarshal feature flag settings: %s", err.Error()) + var ff map[string]interface{} + if err := json.Unmarshal([]byte(*setting.Value), &ff); err != nil { + return nil, fmt.Errorf("failed to unmarshal feature flag settings: %s", err.Error()) + } + + featureFlagReference := fmt.Sprintf("%s/kv/%s", clientEndpoint, *setting.Key) + if setting.Label != nil && strings.TrimSpace(*setting.Label) != "" { + featureFlagReference += fmt.Sprintf("?label=%s", *setting.Label) + } + + populateTelemetryMetadata(ff, setting.ETag, featureFlagReference) + mergedFeatureFlags = append(mergedFeatureFlags, ff) + } + + for _, featureFlag := range enhancedFeatureFlags { + if featureFlag.Name == nil { + continue + } + + featureFlagReference := fmt.Sprintf("%s/ff/%s", clientEndpoint, *featureFlag.Name) + if featureFlag.Label != nil && strings.TrimSpace(*featureFlag.Label) != "" { + featureFlagReference += fmt.Sprintf("?label=%s", *featureFlag.Label) + } + + convertedFF := convertToMicrosoftSchema(featureFlag) + populateTelemetryMetadata(convertedFF, featureFlag.ETag, featureFlagReference) + mergedFeatureFlags = append(mergedFeatureFlags, convertedFF) + } + + // Deduplicate by id keeping the last occurrence so enhanced feature flags supersede classic ones. + return deduplicateFeatureFlags(mergedFeatureFlags), nil +} + +func deduplicateFeatureFlags(featureFlags []map[string]interface{}) map[string]interface{} { + seen := make(map[string]bool, len(featureFlags)) + deduplicated := make([]interface{}, 0, len(featureFlags)) + + for i := len(featureFlags) - 1; i >= 0; i-- { + id, _ := featureFlags[i][FeatureFlagIdKey].(string) + if seen[id] { + continue } - populateTelemetryMetadata(out, settingsResponse.Settings[i], clientEndpoint) - deduplicatedFeatureFlags = append(deduplicatedFeatureFlags, out) + seen[id] = true + deduplicated = append(deduplicated, featureFlags[i]) } - // reverse the deduplicateFeatureFlags to keep the order - for i, j := 0, len(deduplicatedFeatureFlags)-1; i < j; i, j = i+1, j-1 { - deduplicatedFeatureFlags[i], deduplicatedFeatureFlags[j] = deduplicatedFeatureFlags[j], deduplicatedFeatureFlags[i] + // reverse to restore the original order + for i, j := 0, len(deduplicated)-1; i < j; i, j = i+1, j-1 { + deduplicated[i], deduplicated[j] = deduplicated[j], deduplicated[i] } // featureFlagSection = {"feature_flags": [{...}, {...}]} - var featureFlagSection = map[string]interface{}{ - FeatureFlagSectionName: deduplicatedFeatureFlags, + featureFlagSection := map[string]interface{}{ + FeatureFlagSectionName: deduplicated, } - return featureFlagSection, settingsResponse.Etags, nil + return featureFlagSection } func (csl *ConfigurationSettingLoader) ResolveSecretReferences( @@ -700,7 +800,7 @@ func (csl *ConfigurationSettingLoader) ExecuteFailoverPolicy(ctx context.Context return nil, fmt.Errorf("all app configuration clients failed to get settings: %v", errors) } -func updateClientBackoffStatus(clientWrapper *ConfigurationClientWrapper, successful bool) { +func updateClientBackoffStatus(clientWrapper *AppConfigurationClientWrapper, successful bool) { if successful { clientWrapper.BackOffEndTime = metav1.Time{} // Reset FailedAttempts when client succeeded @@ -774,16 +874,20 @@ func GetKeyValueFilters(acpSpec acpv1.AzureAppConfigurationProviderSpec) []acpv1 return deduplicateFilters(normalizeFilter(acpSpec.Configuration.Selectors)) } +func GetEnhancedFeatureFlagFilters(acpSpec acpv1.AzureAppConfigurationProviderSpec) []acpv1.Selector { + if acpSpec.FeatureFlag == nil { + return make([]acpv1.Selector, 0) + } + + return deduplicateFilters(normalizeFilter(acpSpec.FeatureFlag.Selectors)) +} + func GetFeatureFlagFilters(acpSpec acpv1.AzureAppConfigurationProviderSpec) []acpv1.Selector { - featureFlagFilters := make([]acpv1.Selector, 0) - - if acpSpec.FeatureFlag != nil { - featureFlagFilters = deduplicateFilters(normalizeFilter(acpSpec.FeatureFlag.Selectors)) - for i := 0; i < len(featureFlagFilters); i++ { - if featureFlagFilters[i].KeyFilter != nil { - prefixedFeatureFlagFilter := FeatureFlagKeyPrefix + *featureFlagFilters[i].KeyFilter - featureFlagFilters[i].KeyFilter = &prefixedFeatureFlagFilter - } + featureFlagFilters := GetEnhancedFeatureFlagFilters(acpSpec) + for i := 0; i < len(featureFlagFilters); i++ { + if featureFlagFilters[i].KeyFilter != nil { + prefixedFeatureFlagFilter := FeatureFlagKeyPrefix + *featureFlagFilters[i].KeyFilter + featureFlagFilters[i].KeyFilter = &prefixedFeatureFlagFilter } } @@ -1058,7 +1162,7 @@ func MergeSecret(secret map[string]corev1.Secret, newSecret map[string]corev1.Se } // rotates the slice to the left by k positions -func rotate(clients []*ConfigurationClientWrapper, k int) { +func rotate(clients []*AppConfigurationClientWrapper, k int) { n := len(clients) k = k % n if k == 0 { @@ -1072,7 +1176,7 @@ func rotate(clients []*ConfigurationClientWrapper, k int) { reverseClients(clients, n-k, n-1) } -func reverseClients(clients []*ConfigurationClientWrapper, start, end int) { +func reverseClients(clients []*AppConfigurationClientWrapper, start, end int) { for start < end { clients[start], clients[end] = clients[end], clients[start] start++ @@ -1080,18 +1184,7 @@ func reverseClients(clients []*ConfigurationClientWrapper, start, end int) { } } -func generateFeatureFlagReference(setting azappconfig.Setting, endpoint string) string { - featureFlagReference := fmt.Sprintf("%s/kv/%s", endpoint, *setting.Key) - - // Check if the label is present and not empty - if setting.Label != nil && strings.TrimSpace(*setting.Label) != "" { - featureFlagReference += fmt.Sprintf("?label=%s", *setting.Label) - } - - return featureFlagReference -} - -func populateTelemetryMetadata(featureFlag map[string]interface{}, setting azappconfig.Setting, endpoint string) { +func populateTelemetryMetadata(featureFlag map[string]interface{}, eTag *azcore.ETag, featureFlagRef string) { if telemetry, ok := featureFlag[TelemetryKey].(map[string]interface{}); ok { if enabled, ok := telemetry[EnabledKey].(bool); ok && enabled { metadata, _ := telemetry[MetadataKey].(map[string]interface{}) @@ -1100,8 +1193,10 @@ func populateTelemetryMetadata(featureFlag map[string]interface{}, setting azapp } // Set the new metadata - metadata[ETagKey] = *setting.ETag - metadata[FeatureFlagReferenceKey] = generateFeatureFlagReference(setting, endpoint) + if eTag != nil { + metadata[ETagKey] = *eTag + } + metadata[FeatureFlagReferenceKey] = featureFlagRef telemetry[MetadataKey] = metadata } } diff --git a/internal/loader/feature_flag_converter.go b/internal/loader/feature_flag_converter.go new file mode 100644 index 0000000..29f26ed --- /dev/null +++ b/internal/loader/feature_flag_converter.go @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package loader + +import ( + "encoding/json" + + azappconfig "github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2" +) + +// convertToMicrosoftSchema converts an enhanced FeatureFlag returned by new feature flag +// endpoint into the Microsoft Feature Management schema object (snake_case) used within the +// `feature_management.feature_flags` array. +func convertToMicrosoftSchema(featureFlag azappconfig.FeatureFlag) map[string]interface{} { + result := make(map[string]interface{}) + + if featureFlag.Name != nil { + result["id"] = *featureFlag.Name + } + + if featureFlag.Enabled != nil { + result["enabled"] = *featureFlag.Enabled + } else { + result["enabled"] = false + } + + if featureFlag.Description != nil { + result["description"] = *featureFlag.Description + } + + // conditions: filters -> client_filters, requirementType -> requirement_type + conditions := make(map[string]interface{}) + clientFilters := make([]interface{}, 0) + if featureFlag.Conditions != nil { + for _, filter := range featureFlag.Conditions.Filters { + clientFilter := make(map[string]interface{}) + if filter.Name != nil { + clientFilter["name"] = *filter.Name + } + if filter.Parameters != nil { + parameters := make(map[string]interface{}, len(filter.Parameters)) + for key, value := range filter.Parameters { + parameters[key] = parseFeatureFlagValue(value) + } + clientFilter["parameters"] = parameters + } + clientFilters = append(clientFilters, clientFilter) + } + } + conditions["client_filters"] = clientFilters + if featureFlag.Conditions != nil && featureFlag.Conditions.RequirementType != nil { + conditions["requirement_type"] = string(*featureFlag.Conditions.RequirementType) + } + result["conditions"] = conditions + + // variants: value -> configuration_value, statusOverride -> status_override + if featureFlag.Variants != nil { + variants := make([]interface{}, 0, len(featureFlag.Variants)) + for _, variant := range featureFlag.Variants { + variantMap := make(map[string]interface{}) + if variant.Name != nil { + variantMap["name"] = *variant.Name + } + if variant.Value != nil { + variantMap["configuration_value"] = parseFeatureFlagValue(variant.Value) + } + if variant.StatusOverride != nil { + variantMap["status_override"] = string(*variant.StatusOverride) + } + variants = append(variants, variantMap) + } + result["variants"] = variants + } + + // allocation: camelCase -> snake_case + if featureFlag.Allocation != nil { + allocation := make(map[string]interface{}) + source := featureFlag.Allocation + if source.DefaultWhenDisabled != nil { + allocation["default_when_disabled"] = *source.DefaultWhenDisabled + } + if source.DefaultWhenEnabled != nil { + allocation["default_when_enabled"] = *source.DefaultWhenEnabled + } + if source.Percentile != nil { + percentiles := make([]interface{}, 0, len(source.Percentile)) + for _, percentile := range source.Percentile { + percentileMap := make(map[string]interface{}) + if percentile.Variant != nil { + percentileMap["variant"] = *percentile.Variant + } + if percentile.From != nil { + percentileMap["from"] = *percentile.From + } + if percentile.To != nil { + percentileMap["to"] = *percentile.To + } + percentiles = append(percentiles, percentileMap) + } + allocation["percentile"] = percentiles + } + if source.Group != nil { + groups := make([]interface{}, 0, len(source.Group)) + for _, group := range source.Group { + groupMap := make(map[string]interface{}) + if group.Variant != nil { + groupMap["variant"] = *group.Variant + } + if group.Groups != nil { + groupMap["groups"] = toInterfaceSlice(group.Groups) + } + groups = append(groups, groupMap) + } + allocation["group"] = groups + } + if source.User != nil { + users := make([]interface{}, 0, len(source.User)) + for _, user := range source.User { + userMap := make(map[string]interface{}) + if user.Variant != nil { + userMap["variant"] = *user.Variant + } + if user.Users != nil { + userMap["users"] = toInterfaceSlice(user.Users) + } + users = append(users, userMap) + } + allocation["user"] = users + } + if source.Seed != nil { + allocation["seed"] = *source.Seed + } + result["allocation"] = allocation + } + + // telemetry: metadata is (re)populated later by populateTelemetryMetadata with ETag/FeatureFlagReference + if featureFlag.Telemetry != nil { + telemetry := make(map[string]interface{}) + if featureFlag.Telemetry.Enabled != nil { + telemetry["enabled"] = *featureFlag.Telemetry.Enabled + } else { + telemetry["enabled"] = false + } + if featureFlag.Telemetry.Metadata != nil { + metadata := make(map[string]interface{}, len(featureFlag.Telemetry.Metadata)) + for key, value := range featureFlag.Telemetry.Metadata { + if value != nil { + metadata[key] = *value + } + } + telemetry["metadata"] = metadata + } + result["telemetry"] = telemetry + } + + return result +} + +// Attempting to parse the string as JSON recovers booleans, numbers, and nested objects; non-JSON strings are returned as-is. +func parseFeatureFlagValue(raw *string) interface{} { + if raw == nil { + return nil + } + + var parsed interface{} + if err := json.Unmarshal([]byte(*raw), &parsed); err == nil { + return parsed + } + + return *raw +} + +// toInterfaceSlice converts a slice of strings into a slice of interface{} for inclusion in the +// generic map that is marshaled into the feature management schema. +func toInterfaceSlice(values []string) []interface{} { + result := make([]interface{}, len(values)) + for i, value := range values { + result[i] = value + } + return result +} diff --git a/internal/loader/feature_flag_converter_test.go b/internal/loader/feature_flag_converter_test.go new file mode 100644 index 0000000..2a7409e --- /dev/null +++ b/internal/loader/feature_flag_converter_test.go @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package loader + +import ( + acpv1 "azappconfig/provider/api/v1" + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + azappconfig "github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2" +) + +// fakeAppConfigurationClient is a test double for AppConfigurationClient that serves the provided +// pages from in-memory slices. Only the list operations exercised by the feature flag loading path +// are backed by data; the remaining methods return empty results. +type fakeAppConfigurationClient struct { + keyValuePages [][]azappconfig.Setting + featureFlagPages [][]azappconfig.FeatureFlag +} + +func (c *fakeAppConfigurationClient) NewListSettingsPager(_ azappconfig.SettingSelector, _ *azappconfig.ListSettingsOptions) *runtime.Pager[azappconfig.ListSettingsPageResponse] { + pages := c.keyValuePages + if len(pages) == 0 { + pages = [][]azappconfig.Setting{{}} + } + index := 0 + return runtime.NewPager(runtime.PagingHandler[azappconfig.ListSettingsPageResponse]{ + More: func(azappconfig.ListSettingsPageResponse) bool { return index < len(pages) }, + Fetcher: func(context.Context, *azappconfig.ListSettingsPageResponse) (azappconfig.ListSettingsPageResponse, error) { + page := pages[index] + index++ + etag := azcore.ETag(fmt.Sprintf("kv-page-%d", index)) + return azappconfig.ListSettingsPageResponse{Settings: page, ETag: &etag}, nil + }, + }) +} + +func (c *fakeAppConfigurationClient) GetSetting(context.Context, string, *azappconfig.GetSettingOptions) (azappconfig.GetSettingResponse, error) { + return azappconfig.GetSettingResponse{}, nil +} + +func (c *fakeAppConfigurationClient) GetSnapshot(context.Context, string, *azappconfig.GetSnapshotOptions) (azappconfig.GetSnapshotResponse, error) { + return azappconfig.GetSnapshotResponse{}, nil +} + +func (c *fakeAppConfigurationClient) NewListSettingsForSnapshotPager(_ string, _ *azappconfig.ListSettingsForSnapshotOptions) *runtime.Pager[azappconfig.ListSettingsForSnapshotResponse] { + return runtime.NewPager(runtime.PagingHandler[azappconfig.ListSettingsForSnapshotResponse]{ + More: func(azappconfig.ListSettingsForSnapshotResponse) bool { return false }, + Fetcher: func(context.Context, *azappconfig.ListSettingsForSnapshotResponse) (azappconfig.ListSettingsForSnapshotResponse, error) { + return azappconfig.ListSettingsForSnapshotResponse{}, nil + }, + }) +} + +func (c *fakeAppConfigurationClient) NewListFeatureFlagsPager(_ azappconfig.FeatureFlagSelector, _ *azappconfig.ListFeatureFlagsOptions) *runtime.Pager[azappconfig.ListFeatureFlagsPageResponse] { + pages := c.featureFlagPages + if len(pages) == 0 { + pages = [][]azappconfig.FeatureFlag{{}} + } + index := 0 + return runtime.NewPager(runtime.PagingHandler[azappconfig.ListFeatureFlagsPageResponse]{ + More: func(azappconfig.ListFeatureFlagsPageResponse) bool { return index < len(pages) }, + Fetcher: func(context.Context, *azappconfig.ListFeatureFlagsPageResponse) (azappconfig.ListFeatureFlagsPageResponse, error) { + page := pages[index] + index++ + etag := azcore.ETag(fmt.Sprintf("ff-page-%d", index)) + return azappconfig.ListFeatureFlagsPageResponse{FeatureFlags: page, ETag: &etag}, nil + }, + }) +} + +func newTypedFeatureFlag(name string, enabled bool) azappconfig.FeatureFlag { + etag := azcore.ETag("etag-" + name) + return azappconfig.FeatureFlag{ + Name: &name, + Enabled: &enabled, + ETag: &etag, + Conditions: &azappconfig.FeatureFlagConditions{Filters: []azappconfig.FeatureFlagFilter{}}, + } +} + +func TestConvertFeatureFlagToMap(t *testing.T) { + name := "Variant" + enabled := true + filterName := "Microsoft.TimeWindow" + startParam := "Mon, 01 Jan 2024 00:00:00 GMT" + requirementType := azappconfig.RequirementTypeAll + offName, offValue := "Off", "false" + onName, onValue := "On", "true" + statusOverride := azappconfig.StatusOverrideDisabled + defaultVariant := "Off" + percentileVariant := "On" + from, to := 0.0, 50.0 + seed := "seed-value" + telemetryEnabled := true + + featureFlag := azappconfig.FeatureFlag{ + Name: &name, + Enabled: &enabled, + Conditions: &azappconfig.FeatureFlagConditions{ + RequirementType: &requirementType, + Filters: []azappconfig.FeatureFlagFilter{ + {Name: &filterName, Parameters: map[string]*string{"Start": &startParam}}, + }, + }, + Variants: []azappconfig.FeatureFlagVariantDefinition{ + {Name: &offName, Value: &offValue, StatusOverride: &statusOverride}, + {Name: &onName, Value: &onValue}, + }, + Allocation: &azappconfig.FeatureFlagAllocation{ + DefaultWhenEnabled: &defaultVariant, + DefaultWhenDisabled: &defaultVariant, + Percentile: []azappconfig.PercentileAllocation{{Variant: &percentileVariant, From: &from, To: &to}}, + Seed: &seed, + }, + Telemetry: &azappconfig.FeatureFlagTelemetryConfiguration{Enabled: &telemetryEnabled}, + } + + actual, err := json.Marshal(convertToMicrosoftSchema(featureFlag)) + if err != nil { + t.Fatalf("failed to marshal converted feature flag: %s", err) + } + + expected := `{"allocation":{"default_when_disabled":"Off","default_when_enabled":"Off","percentile":[{"from":0,"to":50,"variant":"On"}],"seed":"seed-value"},"conditions":{"client_filters":[{"name":"Microsoft.TimeWindow","parameters":{"Start":"Mon, 01 Jan 2024 00:00:00 GMT"}}],"requirement_type":"All"},"enabled":true,"id":"Variant","telemetry":{"enabled":true},"variants":[{"configuration_value":false,"name":"Off","status_override":"Disabled"},{"configuration_value":true,"name":"On"}]}` + if string(actual) != expected { + t.Errorf("unexpected converted feature flag.\n got: %s\nwant: %s", actual, expected) + } +} + +func TestEnhancedFeatureFlagSettingsClientLoadsEnhancedFlags(t *testing.T) { + endpointNameFilter := "*" + nullLabel := "\x00" + + client := &fakeAppConfigurationClient{ + featureFlagPages: [][]azappconfig.FeatureFlag{{ + newTypedFeatureFlag("Shared", false), + newTypedFeatureFlag("EnhancedOnly", true), + }}, + } + + settingsClient := &EnhancedFeatureFlagSettingsClient{ + enhancedFeatureFlagSelectors: []acpv1.Selector{{KeyFilter: &endpointNameFilter, LabelFilter: &nullLabel}}, + } + + response, err := settingsClient.GetSettings(context.Background(), client) + if err != nil { + t.Fatalf("GetSettings returned error: %s", err) + } + + // The enhanced client only loads flags from the dedicated feature flag endpoint; merging with + // classic feature flags happens later in ProcessFeatureFlags. + if len(response.EnhancedFeatureFlags) != 2 { + t.Fatalf("expected 2 enhanced feature flags, got %d", len(response.EnhancedFeatureFlags)) + } + if response.EnhancedFeatureFlags[0].Name == nil || *response.EnhancedFeatureFlags[0].Name != "Shared" { + t.Errorf("expected first enhanced flag to be 'Shared', got %v", response.EnhancedFeatureFlags[0].Name) + } + if len(response.Settings) != 0 { + t.Errorf("expected no classic settings from the enhanced client, got %d", len(response.Settings)) + } +} + +func TestFeatureFlagEndpointEtagSettingsClientDetectsChanges(t *testing.T) { + nameFilter := "*" + nullLabel := "\x00" + comparable := acpv1.MakeComparable(acpv1.Selector{KeyFilter: &nameFilter, LabelFilter: &nullLabel}) + + client := &fakeAppConfigurationClient{ + featureFlagPages: [][]azappconfig.FeatureFlag{{newTypedFeatureFlag("Beta", true)}}, + } + + // The fake client assigns the first page the ETag "ff-page-1". + unchangedETag := azcore.ETag("ff-page-1") + unchangedClient := &EnhancedFeatureFlagEtagsClient{ + etags: map[acpv1.ComparableSelector][]*azcore.ETag{comparable: {&unchangedETag}}, + } + unchangedResponse, err := unchangedClient.GetSettings(context.Background(), client) + if err != nil { + t.Fatalf("GetSettings returned error: %s", err) + } + if unchangedResponse.Etags != nil { + t.Errorf("expected no change to be detected when page ETags match") + } + + staleETag := azcore.ETag("stale-etag") + changedClient := &EnhancedFeatureFlagEtagsClient{ + etags: map[acpv1.ComparableSelector][]*azcore.ETag{comparable: {&staleETag}}, + } + changedResponse, err := changedClient.GetSettings(context.Background(), client) + if err != nil { + t.Fatalf("GetSettings returned error: %s", err) + } + if changedResponse.Etags == nil { + t.Errorf("expected a change to be detected when page ETags differ") + } +} diff --git a/internal/loader/mocks/mock_configuration_settings_retriever.go b/internal/loader/mocks/mock_configuration_settings_retriever.go index 446f7f6..00c6a34 100644 --- a/internal/loader/mocks/mock_configuration_settings_retriever.go +++ b/internal/loader/mocks/mock_configuration_settings_retriever.go @@ -53,6 +53,21 @@ func (mr *MockConfigurationSettingsRetrieverMockRecorder) CheckAndRefreshSentine return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckAndRefreshSentinels", reflect.TypeOf((*MockConfigurationSettingsRetriever)(nil).CheckAndRefreshSentinels), arg0, arg1, arg2) } +// CheckIfEnhancedFeatureFlagsChanged mocks base method. +func (m *MockConfigurationSettingsRetriever) CheckIfEnhancedFeatureFlagsChanged(arg0 context.Context, arg1 map[v1.ComparableSelector][]*azcore.ETag) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckIfEnhancedFeatureFlagsChanged", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CheckIfEnhancedFeatureFlagsChanged indicates an expected call of CheckIfEnhancedFeatureFlagsChanged. +func (mr *MockConfigurationSettingsRetrieverMockRecorder) CheckIfEnhancedFeatureFlagsChanged(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckIfEnhancedFeatureFlagsChanged", reflect.TypeOf((*MockConfigurationSettingsRetriever)(nil).CheckIfEnhancedFeatureFlagsChanged), arg0, arg1) +} + // CheckPageETags mocks base method. func (m *MockConfigurationSettingsRetriever) CheckPageETags(arg0 context.Context, arg1 map[v1.ComparableSelector][]*azcore.ETag) (bool, error) { m.ctrl.T.Helper() diff --git a/internal/loader/request_tracing.go b/internal/loader/request_tracing.go index 710b29e..e1eee40 100644 --- a/internal/loader/request_tracing.go +++ b/internal/loader/request_tracing.go @@ -24,6 +24,7 @@ type TracingFeatures struct { UseAIConfiguration bool UseAIChatCompletionConfiguration bool UseSnapshotReference bool + UseEnhancedFeatureFlag bool } // Feature flag telemetry @@ -49,6 +50,7 @@ const ( AIConfigurationKey string = "AI" AIChatCompletionKey string = "AICC" SnapshotReferenceKey string = "SnapshotRef" + EnhancedFeatureFlagKey string = "EnhFF" ) func createCorrelationContextHeader(ctx context.Context, provider acpv1.AzureAppConfigurationProvider, tracingFeatures TracingFeatures) http.Header { @@ -103,6 +105,10 @@ func createCorrelationContextHeader(ctx context.Context, provider acpv1.AzureApp features = append(features, SnapshotReferenceKey) } + if tracingFeatures.UseEnhancedFeatureFlag { + features = append(features, EnhancedFeatureFlagKey) + } + if len(features) > 0 { featureStr := "Features=" + strings.Join(features, TracingFeatureDelimiterKey) output = append(output, featureStr) diff --git a/internal/loader/settings_client.go b/internal/loader/settings_client.go index adeb575..f336692 100644 --- a/internal/loader/settings_client.go +++ b/internal/loader/settings_client.go @@ -17,8 +17,9 @@ import ( //go:generate mockgen -destination=mocks/mock_settings_client.go -package mocks . SettingsClient type SettingsResponse struct { - Settings []azappconfig.Setting - Etags map[acpv1.ComparableSelector][]*azcore.ETag + Settings []azappconfig.Setting + Etags map[acpv1.ComparableSelector][]*azcore.ETag + EnhancedFeatureFlags []azappconfig.FeatureFlag } type EtagSettingsClient struct { @@ -35,11 +36,21 @@ type SelectorSettingsClient struct { selectors []acpv1.Selector } +// EnhancedFeatureFlagEtagsClient is used to check if the enhanced feature flags have changed +type EnhancedFeatureFlagEtagsClient struct { + etags map[acpv1.ComparableSelector][]*azcore.ETag +} + +// EnhancedFeatureFlagSettingsClient loads enhanced feature flags +type EnhancedFeatureFlagSettingsClient struct { + enhancedFeatureFlagSelectors []acpv1.Selector +} + type SettingsClient interface { - GetSettings(ctx context.Context, client *azappconfig.Client) (*SettingsResponse, error) + GetSettings(ctx context.Context, client AppConfigurationClient) (*SettingsResponse, error) } -func (s *EtagSettingsClient) GetSettings(ctx context.Context, client *azappconfig.Client) (*SettingsResponse, error) { +func (s *EtagSettingsClient) GetSettings(ctx context.Context, client AppConfigurationClient) (*SettingsResponse, error) { nullString := "\x00" settingsResponse := &SettingsResponse{} for comparableFilter, pageEtags := range s.etags { @@ -90,7 +101,7 @@ func (s *EtagSettingsClient) GetSettings(ctx context.Context, client *azappconfi return settingsResponse, nil } -func (s *SentinelSettingsClient) GetSettings(ctx context.Context, client *azappconfig.Client) (*SettingsResponse, error) { +func (s *SentinelSettingsClient) GetSettings(ctx context.Context, client AppConfigurationClient) (*SettingsResponse, error) { sentinelSetting, err := client.GetSetting(ctx, s.sentinel.Key, &azappconfig.GetSettingOptions{Label: s.sentinel.Label, OnlyIfChanged: s.etag}) if err != nil { var respErr *azcore.ResponseError @@ -121,7 +132,7 @@ func (s *SentinelSettingsClient) GetSettings(ctx context.Context, client *azappc }, nil } -func (s *SelectorSettingsClient) GetSettings(ctx context.Context, client *azappconfig.Client) (*SettingsResponse, error) { +func (s *SelectorSettingsClient) GetSettings(ctx context.Context, client AppConfigurationClient) (*SettingsResponse, error) { settings := make([]azappconfig.Setting, 0) pageEtags := make(map[acpv1.ComparableSelector][]*azcore.ETag) @@ -162,7 +173,7 @@ func (s *SelectorSettingsClient) GetSettings(ctx context.Context, client *azappc }, nil } -func loadSnapshotSettings(ctx context.Context, client *azappconfig.Client, snapshotName string) ([]azappconfig.Setting, error) { +func loadSnapshotSettings(ctx context.Context, client AppConfigurationClient, snapshotName string) ([]azappconfig.Setting, error) { settings := make([]azappconfig.Setting, 0) snapshot, err := client.GetSnapshot(ctx, snapshotName, nil) if err != nil { @@ -189,3 +200,89 @@ func loadSnapshotSettings(ctx context.Context, client *azappconfig.Client, snaps return settings, nil } + +func (s *EnhancedFeatureFlagSettingsClient) GetSettings(ctx context.Context, client AppConfigurationClient) (*SettingsResponse, error) { + enhancedFeatureFlags := make([]azappconfig.FeatureFlag, 0) + pageEtags := make(map[acpv1.ComparableSelector][]*azcore.ETag) + + for _, filter := range s.enhancedFeatureFlagSelectors { + if filter.KeyFilter != nil { + selector := azappconfig.FeatureFlagSelector{ + NameFilter: filter.KeyFilter, + LabelFilter: filter.LabelFilter, + TagsFilter: filter.TagFilters, + Fields: azappconfig.AllFeatureFlagFields(), + } + pager := client.NewListFeatureFlagsPager(selector, nil) + latestEtags := make([]*azcore.ETag, 0) + + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, err + } else if page.FeatureFlags != nil { + enhancedFeatureFlags = append(enhancedFeatureFlags, page.FeatureFlags...) + latestEtags = append(latestEtags, page.ETag) + } + } + // update the etags for the filter + pageEtags[acpv1.MakeComparable(filter)] = latestEtags + } + } + + return &SettingsResponse{ + EnhancedFeatureFlags: enhancedFeatureFlags, + Etags: pageEtags, + }, nil +} + +func (s *EnhancedFeatureFlagEtagsClient) GetSettings(ctx context.Context, client AppConfigurationClient) (*SettingsResponse, error) { + settingsResponse := &SettingsResponse{} + for comparableFilter, storedETags := range s.etags { + filter := acpv1.FromComparable(comparableFilter) + if filter.KeyFilter != nil { + selector := azappconfig.FeatureFlagSelector{ + NameFilter: filter.KeyFilter, + LabelFilter: filter.LabelFilter, + TagsFilter: filter.TagFilters, + Fields: azappconfig.AllFeatureFlagFields(), + } + + pager := client.NewListFeatureFlagsPager(selector, nil) + latestETags := make([]*azcore.ETag, 0) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, err + } + latestETags = append(latestETags, page.ETag) + } + + if !equalETagSlices(storedETags, latestETags) { + settingsResponse.Etags = make(map[acpv1.ComparableSelector][]*azcore.ETag) + return settingsResponse, nil + } + } + } + + return settingsResponse, nil +} + +// equalETagSlices reports whether two ordered slices of page ETags are equivalent. +func equalETagSlices(a, b []*azcore.ETag) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] == nil || b[i] == nil { + if a[i] != b[i] { + return false + } + continue + } + if *a[i] != *b[i] { + return false + } + } + return true +} From 40e4201bb8417ac5d6ed9a7b9de9bf75ffa24e98 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Fri, 14 Aug 2026 09:55:24 +0800 Subject: [PATCH 2/6] update conditional request --- .../loader/feature_flag_converter_test.go | 36 ++++++++++++++-- internal/loader/settings_client.go | 42 ++++++++----------- 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/internal/loader/feature_flag_converter_test.go b/internal/loader/feature_flag_converter_test.go index 2a7409e..00fff85 100644 --- a/internal/loader/feature_flag_converter_test.go +++ b/internal/loader/feature_flag_converter_test.go @@ -19,8 +19,9 @@ import ( // pages from in-memory slices. Only the list operations exercised by the feature flag loading path // are backed by data; the remaining methods return empty results. type fakeAppConfigurationClient struct { - keyValuePages [][]azappconfig.Setting - featureFlagPages [][]azappconfig.FeatureFlag + keyValuePages [][]azappconfig.Setting + featureFlagPages [][]azappconfig.FeatureFlag + featureFlagListOptions *azappconfig.ListFeatureFlagsOptions } func (c *fakeAppConfigurationClient) NewListSettingsPager(_ azappconfig.SettingSelector, _ *azappconfig.ListSettingsOptions) *runtime.Pager[azappconfig.ListSettingsPageResponse] { @@ -57,7 +58,8 @@ func (c *fakeAppConfigurationClient) NewListSettingsForSnapshotPager(_ string, _ }) } -func (c *fakeAppConfigurationClient) NewListFeatureFlagsPager(_ azappconfig.FeatureFlagSelector, _ *azappconfig.ListFeatureFlagsOptions) *runtime.Pager[azappconfig.ListFeatureFlagsPageResponse] { +func (c *fakeAppConfigurationClient) NewListFeatureFlagsPager(_ azappconfig.FeatureFlagSelector, options *azappconfig.ListFeatureFlagsOptions) *runtime.Pager[azappconfig.ListFeatureFlagsPageResponse] { + c.featureFlagListOptions = options pages := c.featureFlagPages if len(pages) == 0 { pages = [][]azappconfig.FeatureFlag{{}} @@ -69,6 +71,12 @@ func (c *fakeAppConfigurationClient) NewListFeatureFlagsPager(_ azappconfig.Feat page := pages[index] index++ etag := azcore.ETag(fmt.Sprintf("ff-page-%d", index)) + if options != nil && index <= len(options.MatchConditions) { + condition := options.MatchConditions[index-1] + if condition.IfNoneMatch != nil && *condition.IfNoneMatch == etag { + return azappconfig.ListFeatureFlagsPageResponse{}, nil + } + } return azappconfig.ListFeatureFlagsPageResponse{FeatureFlags: page, ETag: &etag}, nil }, }) @@ -165,7 +173,7 @@ func TestEnhancedFeatureFlagSettingsClientLoadsEnhancedFlags(t *testing.T) { } } -func TestFeatureFlagEndpointEtagSettingsClientDetectsChanges(t *testing.T) { +func TestEnhancedFeatureFlagEtagsClientUsesConditionalRequests(t *testing.T) { nameFilter := "*" nullLabel := "\x00" comparable := acpv1.MakeComparable(acpv1.Selector{KeyFilter: &nameFilter, LabelFilter: &nullLabel}) @@ -186,6 +194,13 @@ func TestFeatureFlagEndpointEtagSettingsClientDetectsChanges(t *testing.T) { if unchangedResponse.Etags != nil { t.Errorf("expected no change to be detected when page ETags match") } + if client.featureFlagListOptions == nil || len(client.featureFlagListOptions.MatchConditions) != 1 { + t.Fatalf("expected one match condition, got %#v", client.featureFlagListOptions) + } + if client.featureFlagListOptions.MatchConditions[0].IfNoneMatch == nil || + *client.featureFlagListOptions.MatchConditions[0].IfNoneMatch != unchangedETag { + t.Errorf("expected If-None-Match %q", unchangedETag) + } staleETag := azcore.ETag("stale-etag") changedClient := &EnhancedFeatureFlagEtagsClient{ @@ -198,4 +213,17 @@ func TestFeatureFlagEndpointEtagSettingsClientDetectsChanges(t *testing.T) { if changedResponse.Etags == nil { t.Errorf("expected a change to be detected when page ETags differ") } + + missingPageClient := &EnhancedFeatureFlagEtagsClient{ + etags: map[acpv1.ComparableSelector][]*azcore.ETag{ + comparable: {&unchangedETag, &staleETag}, + }, + } + missingPageResponse, err := missingPageClient.GetSettings(context.Background(), client) + if err != nil { + t.Fatalf("GetSettings returned error: %s", err) + } + if missingPageResponse.Etags == nil { + t.Errorf("expected a change to be detected when the page count differs") + } } diff --git a/internal/loader/settings_client.go b/internal/loader/settings_client.go index f336692..7065677 100644 --- a/internal/loader/settings_client.go +++ b/internal/loader/settings_client.go @@ -238,7 +238,7 @@ func (s *EnhancedFeatureFlagSettingsClient) GetSettings(ctx context.Context, cli func (s *EnhancedFeatureFlagEtagsClient) GetSettings(ctx context.Context, client AppConfigurationClient) (*SettingsResponse, error) { settingsResponse := &SettingsResponse{} - for comparableFilter, storedETags := range s.etags { + for comparableFilter, pageEtags := range s.etags { filter := acpv1.FromComparable(comparableFilter) if filter.KeyFilter != nil { selector := azappconfig.FeatureFlagSelector{ @@ -248,17 +248,30 @@ func (s *EnhancedFeatureFlagEtagsClient) GetSettings(ctx context.Context, client Fields: azappconfig.AllFeatureFlagFields(), } - pager := client.NewListFeatureFlagsPager(selector, nil) - latestETags := make([]*azcore.ETag, 0) + conditions := make([]azcore.MatchConditions, 0, len(pageEtags)) + for _, etag := range pageEtags { + conditions = append(conditions, azcore.MatchConditions{IfNoneMatch: etag}) + } + + pager := client.NewListFeatureFlagsPager(selector, &azappconfig.ListFeatureFlagsOptions{ + MatchConditions: conditions, + }) + + pageCount := 0 for pager.More() { + pageCount++ page, err := pager.NextPage(ctx) if err != nil { return nil, err } - latestETags = append(latestETags, page.ETag) + // A conditional request returns a nil ETag for an unchanged (304) page. + if page.ETag != nil { + settingsResponse.Etags = make(map[acpv1.ComparableSelector][]*azcore.ETag) + return settingsResponse, nil + } } - if !equalETagSlices(storedETags, latestETags) { + if pageCount != len(pageEtags) { settingsResponse.Etags = make(map[acpv1.ComparableSelector][]*azcore.ETag) return settingsResponse, nil } @@ -267,22 +280,3 @@ func (s *EnhancedFeatureFlagEtagsClient) GetSettings(ctx context.Context, client return settingsResponse, nil } - -// equalETagSlices reports whether two ordered slices of page ETags are equivalent. -func equalETagSlices(a, b []*azcore.ETag) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i] == nil || b[i] == nil { - if a[i] != b[i] { - return false - } - continue - } - if *a[i] != *b[i] { - return false - } - } - return true -} From a5b0af1a5a00b541e9655b2d488d074039cc9a1d Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Mon, 17 Aug 2026 16:50:12 +0800 Subject: [PATCH 3/6] resolve comments --- internal/controller/processor_test.go | 2 +- internal/loader/app_configuration_client.go | 2 +- internal/loader/configuration_setting_loader.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/controller/processor_test.go b/internal/controller/processor_test.go index 1931393..6c46e74 100644 --- a/internal/controller/processor_test.go +++ b/internal/controller/processor_test.go @@ -36,7 +36,7 @@ var _ = Describe("AppConfiguationProvider processor", func() { BeforeEach(func() { mockCtrl = gomock.NewController(GinkgoT()) mockConfigurationSettings = mocks.NewMockConfigurationSettingsRetriever(mockCtrl) - // The dedicated feature flag endpoint is checked whenever the classic feature flag page ETags + // The enhanced feature flag endpoint is checked whenever the feature flag page ETags // are unchanged; default to reporting no change so existing scenarios are unaffected. mockConfigurationSettings.EXPECT().CheckIfEnhancedFeatureFlagsChanged(gomock.Any(), gomock.Any()).Return(false, nil).AnyTimes() }) diff --git a/internal/loader/app_configuration_client.go b/internal/loader/app_configuration_client.go index 38703ea..86baa8a 100644 --- a/internal/loader/app_configuration_client.go +++ b/internal/loader/app_configuration_client.go @@ -19,7 +19,7 @@ type AppConfigurationClient interface { GetSnapshot(ctx context.Context, snapshotName string, options *azappconfig.GetSnapshotOptions) (azappconfig.GetSnapshotResponse, error) NewListSettingsForSnapshotPager(snapshotName string, options *azappconfig.ListSettingsForSnapshotOptions) *runtime.Pager[azappconfig.ListSettingsForSnapshotResponse] - // Feature flag operations served by the dedicated feature flag endpoint. + // Feature flag operations served by the enhanced feature flag endpoint. NewListFeatureFlagsPager(selector azappconfig.FeatureFlagSelector, options *azappconfig.ListFeatureFlagsOptions) *runtime.Pager[azappconfig.ListFeatureFlagsPageResponse] } diff --git a/internal/loader/configuration_setting_loader.go b/internal/loader/configuration_setting_loader.go index 411f3ba..bcbbe31 100644 --- a/internal/loader/configuration_setting_loader.go +++ b/internal/loader/configuration_setting_loader.go @@ -510,7 +510,7 @@ func (csl *ConfigurationSettingLoader) CheckIfEnhancedFeatureFlagsChanged(ctx co return false, err } - // a non-nil Etags map signals that the feature flag endpoint page ETags changed + // a non-nil Etags map signals that the enhanced feature flag endpoint page ETags changed return settingsResponse.Etags != nil, nil } From a49ecdf1998668864ecfa2ed219b4bc90b690629 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 20 Aug 2026 13:19:06 +0800 Subject: [PATCH 4/6] update enhanced ff filter parameter/variant value handling --- .../loader/configuration_setting_loader.go | 11 +- internal/loader/feature_flag_converter.go | 135 +++++++++++++----- .../loader/feature_flag_converter_test.go | 121 +++++++++++++++- 3 files changed, 222 insertions(+), 45 deletions(-) diff --git a/internal/loader/configuration_setting_loader.go b/internal/loader/configuration_setting_loader.go index bcbbe31..e145efc 100644 --- a/internal/loader/configuration_setting_loader.go +++ b/internal/loader/configuration_setting_loader.go @@ -99,6 +99,8 @@ const ( FeatureFlagSectionName string = "feature_flags" FeatureManagementSectionName string = "feature_management" FeatureFlagIdKey string = "id" + KeyValueResourceType string = "kv" + FeatureFlagResourceType string = "ff" PreservedSecretTypeTag string = ".kubernetes.secret.type" CertTypePem string = "application/x-pem-file" CertTypePfx string = "application/x-pkcs12" @@ -565,7 +567,7 @@ func (csl *ConfigurationSettingLoader) ProcessFeatureFlags(featureFlags []azappc return nil, fmt.Errorf("failed to unmarshal feature flag settings: %s", err.Error()) } - featureFlagReference := fmt.Sprintf("%s/kv/%s", clientEndpoint, *setting.Key) + featureFlagReference := fmt.Sprintf("%s/%s/%s", clientEndpoint, KeyValueResourceType, *setting.Key) if setting.Label != nil && strings.TrimSpace(*setting.Label) != "" { featureFlagReference += fmt.Sprintf("?label=%s", *setting.Label) } @@ -579,12 +581,15 @@ func (csl *ConfigurationSettingLoader) ProcessFeatureFlags(featureFlags []azappc continue } - featureFlagReference := fmt.Sprintf("%s/ff/%s", clientEndpoint, *featureFlag.Name) + featureFlagReference := fmt.Sprintf("%s/%s/%s", clientEndpoint, FeatureFlagResourceType, *featureFlag.Name) if featureFlag.Label != nil && strings.TrimSpace(*featureFlag.Label) != "" { featureFlagReference += fmt.Sprintf("?label=%s", *featureFlag.Label) } - convertedFF := convertToMicrosoftSchema(featureFlag) + convertedFF, err := convertToMicrosoftSchema(featureFlag) + if err != nil { + return nil, fmt.Errorf("Enhanced feature flag '%s': %w", *featureFlag.Name, err) + } populateTelemetryMetadata(convertedFF, featureFlag.ETag, featureFlagReference) mergedFeatureFlags = append(mergedFeatureFlags, convertedFF) } diff --git a/internal/loader/feature_flag_converter.go b/internal/loader/feature_flag_converter.go index 29f26ed..88cee02 100644 --- a/internal/loader/feature_flag_converter.go +++ b/internal/loader/feature_flag_converter.go @@ -5,28 +5,55 @@ package loader import ( "encoding/json" + "fmt" + "strings" + "unicode" azappconfig "github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2" ) +const ( + featureFlagDescriptionKey = "description" + featureFlagConditionsKey = "conditions" + featureFlagClientFiltersKey = "client_filters" + featureFlagRequirementTypeKey = "requirement_type" + featureFlagNameKey = "name" + featureFlagParametersKey = "parameters" + featureFlagVariantsKey = "variants" + featureFlagConfigurationValueKey = "configuration_value" + featureFlagStatusOverrideKey = "status_override" + featureFlagAllocationKey = "allocation" + featureFlagDefaultWhenDisabledKey = "default_when_disabled" + featureFlagDefaultWhenEnabledKey = "default_when_enabled" + featureFlagPercentileKey = "percentile" + featureFlagVariantKey = "variant" + featureFlagFromKey = "from" + featureFlagToKey = "to" + featureFlagGroupKey = "group" + featureFlagGroupsKey = "groups" + featureFlagUserKey = "user" + featureFlagUsersKey = "users" + featureFlagSeedKey = "seed" +) + // convertToMicrosoftSchema converts an enhanced FeatureFlag returned by new feature flag // endpoint into the Microsoft Feature Management schema object (snake_case) used within the // `feature_management.feature_flags` array. -func convertToMicrosoftSchema(featureFlag azappconfig.FeatureFlag) map[string]interface{} { +func convertToMicrosoftSchema(featureFlag azappconfig.FeatureFlag) (map[string]interface{}, error) { result := make(map[string]interface{}) if featureFlag.Name != nil { - result["id"] = *featureFlag.Name + result[FeatureFlagIdKey] = *featureFlag.Name } if featureFlag.Enabled != nil { - result["enabled"] = *featureFlag.Enabled + result[EnabledKey] = *featureFlag.Enabled } else { - result["enabled"] = false + result[EnabledKey] = false } if featureFlag.Description != nil { - result["description"] = *featureFlag.Description + result[featureFlagDescriptionKey] = *featureFlag.Description } // conditions: filters -> client_filters, requirementType -> requirement_type @@ -36,23 +63,23 @@ func convertToMicrosoftSchema(featureFlag azappconfig.FeatureFlag) map[string]in for _, filter := range featureFlag.Conditions.Filters { clientFilter := make(map[string]interface{}) if filter.Name != nil { - clientFilter["name"] = *filter.Name + clientFilter[featureFlagNameKey] = *filter.Name } if filter.Parameters != nil { parameters := make(map[string]interface{}, len(filter.Parameters)) for key, value := range filter.Parameters { - parameters[key] = parseFeatureFlagValue(value) + parameters[key] = parseFeatureFlagParameterValue(value) } - clientFilter["parameters"] = parameters + clientFilter[featureFlagParametersKey] = parameters } clientFilters = append(clientFilters, clientFilter) } } - conditions["client_filters"] = clientFilters + conditions[featureFlagClientFiltersKey] = clientFilters if featureFlag.Conditions != nil && featureFlag.Conditions.RequirementType != nil { - conditions["requirement_type"] = string(*featureFlag.Conditions.RequirementType) + conditions[featureFlagRequirementTypeKey] = string(*featureFlag.Conditions.RequirementType) } - result["conditions"] = conditions + result[featureFlagConditionsKey] = conditions // variants: value -> configuration_value, statusOverride -> status_override if featureFlag.Variants != nil { @@ -60,17 +87,29 @@ func convertToMicrosoftSchema(featureFlag azappconfig.FeatureFlag) map[string]in for _, variant := range featureFlag.Variants { variantMap := make(map[string]interface{}) if variant.Name != nil { - variantMap["name"] = *variant.Name + variantMap[featureFlagNameKey] = *variant.Name } if variant.Value != nil { - variantMap["configuration_value"] = parseFeatureFlagValue(variant.Value) + if isJsonContentType(variant.ContentType) { + parsedValue, err := parseFeatureFlagVariantValue(variant.Value) + if err != nil { + variantName := "" + if variant.Name != nil { + variantName = *variant.Name + } + return nil, fmt.Errorf("failed to parse variant %q value: %w", variantName, err) + } + variantMap[featureFlagConfigurationValueKey] = parsedValue + } else { + variantMap[featureFlagConfigurationValueKey] = *variant.Value + } } if variant.StatusOverride != nil { - variantMap["status_override"] = string(*variant.StatusOverride) + variantMap[featureFlagStatusOverrideKey] = string(*variant.StatusOverride) } variants = append(variants, variantMap) } - result["variants"] = variants + result[featureFlagVariantsKey] = variants } // allocation: camelCase -> snake_case @@ -78,69 +117,69 @@ func convertToMicrosoftSchema(featureFlag azappconfig.FeatureFlag) map[string]in allocation := make(map[string]interface{}) source := featureFlag.Allocation if source.DefaultWhenDisabled != nil { - allocation["default_when_disabled"] = *source.DefaultWhenDisabled + allocation[featureFlagDefaultWhenDisabledKey] = *source.DefaultWhenDisabled } if source.DefaultWhenEnabled != nil { - allocation["default_when_enabled"] = *source.DefaultWhenEnabled + allocation[featureFlagDefaultWhenEnabledKey] = *source.DefaultWhenEnabled } if source.Percentile != nil { percentiles := make([]interface{}, 0, len(source.Percentile)) for _, percentile := range source.Percentile { percentileMap := make(map[string]interface{}) if percentile.Variant != nil { - percentileMap["variant"] = *percentile.Variant + percentileMap[featureFlagVariantKey] = *percentile.Variant } if percentile.From != nil { - percentileMap["from"] = *percentile.From + percentileMap[featureFlagFromKey] = *percentile.From } if percentile.To != nil { - percentileMap["to"] = *percentile.To + percentileMap[featureFlagToKey] = *percentile.To } percentiles = append(percentiles, percentileMap) } - allocation["percentile"] = percentiles + allocation[featureFlagPercentileKey] = percentiles } if source.Group != nil { groups := make([]interface{}, 0, len(source.Group)) for _, group := range source.Group { groupMap := make(map[string]interface{}) if group.Variant != nil { - groupMap["variant"] = *group.Variant + groupMap[featureFlagVariantKey] = *group.Variant } if group.Groups != nil { - groupMap["groups"] = toInterfaceSlice(group.Groups) + groupMap[featureFlagGroupsKey] = toInterfaceSlice(group.Groups) } groups = append(groups, groupMap) } - allocation["group"] = groups + allocation[featureFlagGroupKey] = groups } if source.User != nil { users := make([]interface{}, 0, len(source.User)) for _, user := range source.User { userMap := make(map[string]interface{}) if user.Variant != nil { - userMap["variant"] = *user.Variant + userMap[featureFlagVariantKey] = *user.Variant } if user.Users != nil { - userMap["users"] = toInterfaceSlice(user.Users) + userMap[featureFlagUsersKey] = toInterfaceSlice(user.Users) } users = append(users, userMap) } - allocation["user"] = users + allocation[featureFlagUserKey] = users } if source.Seed != nil { - allocation["seed"] = *source.Seed + allocation[featureFlagSeedKey] = *source.Seed } - result["allocation"] = allocation + result[featureFlagAllocationKey] = allocation } // telemetry: metadata is (re)populated later by populateTelemetryMetadata with ETag/FeatureFlagReference if featureFlag.Telemetry != nil { telemetry := make(map[string]interface{}) if featureFlag.Telemetry.Enabled != nil { - telemetry["enabled"] = *featureFlag.Telemetry.Enabled + telemetry[EnabledKey] = *featureFlag.Telemetry.Enabled } else { - telemetry["enabled"] = false + telemetry[EnabledKey] = false } if featureFlag.Telemetry.Metadata != nil { metadata := make(map[string]interface{}, len(featureFlag.Telemetry.Metadata)) @@ -149,28 +188,46 @@ func convertToMicrosoftSchema(featureFlag azappconfig.FeatureFlag) map[string]in metadata[key] = *value } } - telemetry["metadata"] = metadata + telemetry[MetadataKey] = metadata } - result["telemetry"] = telemetry + result[TelemetryKey] = telemetry } - return result + return result, nil } -// Attempting to parse the string as JSON recovers booleans, numbers, and nested objects; non-JSON strings are returned as-is. -func parseFeatureFlagValue(raw *string) interface{} { +// parseFeatureFlagParameterValue parses object and array parameters as JSON. +// Other values and malformed JSON are preserved as literal strings. +func parseFeatureFlagParameterValue(raw *string) interface{} { if raw == nil { return nil } - var parsed interface{} - if err := json.Unmarshal([]byte(*raw), &parsed); err == nil { - return parsed + trimmed := strings.TrimLeftFunc(*raw, unicode.IsSpace) + if len(trimmed) > 0 && (trimmed[0] == '{' || trimmed[0] == '[') { + var parsed interface{} + if err := json.Unmarshal([]byte(*raw), &parsed); err == nil { + return parsed + } } return *raw } +// parseFeatureFlagVariantValue parses the JSON-encoded value returned by the enhanced feature flag endpoint. +func parseFeatureFlagVariantValue(raw *string) (interface{}, error) { + if raw == nil { + return nil, nil + } + + var parsed interface{} + if err := json.Unmarshal([]byte(*raw), &parsed); err != nil { + return nil, err + } + + return parsed, nil +} + // toInterfaceSlice converts a slice of strings into a slice of interface{} for inclusion in the // generic map that is marshaled into the feature management schema. func toInterfaceSlice(values []string) []interface{} { diff --git a/internal/loader/feature_flag_converter_test.go b/internal/loader/feature_flag_converter_test.go index 00fff85..b625409 100644 --- a/internal/loader/feature_flag_converter_test.go +++ b/internal/loader/feature_flag_converter_test.go @@ -8,6 +8,8 @@ import ( "context" "encoding/json" "fmt" + "reflect" + "strings" "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore" @@ -100,6 +102,7 @@ func TestConvertFeatureFlagToMap(t *testing.T) { requirementType := azappconfig.RequirementTypeAll offName, offValue := "Off", "false" onName, onValue := "On", "true" + jsonContentType := "application/json" statusOverride := azappconfig.StatusOverrideDisabled defaultVariant := "Off" percentileVariant := "On" @@ -117,8 +120,8 @@ func TestConvertFeatureFlagToMap(t *testing.T) { }, }, Variants: []azappconfig.FeatureFlagVariantDefinition{ - {Name: &offName, Value: &offValue, StatusOverride: &statusOverride}, - {Name: &onName, Value: &onValue}, + {Name: &offName, Value: &offValue, ContentType: &jsonContentType, StatusOverride: &statusOverride}, + {Name: &onName, Value: &onValue, ContentType: &jsonContentType}, }, Allocation: &azappconfig.FeatureFlagAllocation{ DefaultWhenEnabled: &defaultVariant, @@ -129,7 +132,12 @@ func TestConvertFeatureFlagToMap(t *testing.T) { Telemetry: &azappconfig.FeatureFlagTelemetryConfiguration{Enabled: &telemetryEnabled}, } - actual, err := json.Marshal(convertToMicrosoftSchema(featureFlag)) + converted, err := convertToMicrosoftSchema(featureFlag) + if err != nil { + t.Fatalf("failed to convert feature flag: %s", err) + } + + actual, err := json.Marshal(converted) if err != nil { t.Fatalf("failed to marshal converted feature flag: %s", err) } @@ -140,6 +148,113 @@ func TestConvertFeatureFlagToMap(t *testing.T) { } } +func TestConvertFeatureFlagFilterParameterValues(t *testing.T) { + featureFlagName := "ParameterFlag" + filterName := "CustomFilter" + objectValue := ` {"key":"value"}` + arrayValue := "\t[1,true]" + invalidObjectValue := " {invalid" + numberValue := "50" + booleanValue := "true" + jsonStringValue := `"quoted"` + emptyValue := "" + + featureFlag := azappconfig.FeatureFlag{ + Name: &featureFlagName, + Conditions: &azappconfig.FeatureFlagConditions{ + Filters: []azappconfig.FeatureFlagFilter{{ + Name: &filterName, + Parameters: map[string]*string{ + "object": &objectValue, + "array": &arrayValue, + "invalid": &invalidObjectValue, + "number": &numberValue, + "boolean": &booleanValue, + "json_string": &jsonStringValue, + "empty": &emptyValue, + "nil": nil, + }, + }}, + }, + } + + converted, err := convertToMicrosoftSchema(featureFlag) + if err != nil { + t.Fatalf("failed to convert feature flag: %s", err) + } + + conditions := converted[featureFlagConditionsKey].(map[string]interface{}) + clientFilters := conditions[featureFlagClientFiltersKey].([]interface{}) + parameters := clientFilters[0].(map[string]interface{})[featureFlagParametersKey].(map[string]interface{}) + expected := map[string]interface{}{ + "object": map[string]interface{}{"key": "value"}, + "array": []interface{}{float64(1), true}, + "invalid": invalidObjectValue, + "number": numberValue, + "boolean": booleanValue, + "json_string": jsonStringValue, + "empty": emptyValue, + "nil": nil, + } + + if !reflect.DeepEqual(parameters, expected) { + t.Errorf("unexpected converted parameters.\n got: %#v\nwant: %#v", parameters, expected) + } +} + +func TestProcessFeatureFlagsReturnsEnhancedConversionError(t *testing.T) { + featureFlagName := "BrokenFlag" + variantName := "BrokenVariant" + invalidValue := "{invalid" + jsonContentType := "application/json" + featureFlag := azappconfig.FeatureFlag{ + Name: &featureFlagName, + Variants: []azappconfig.FeatureFlagVariantDefinition{ + {Name: &variantName, Value: &invalidValue, ContentType: &jsonContentType}, + }, + } + + loader := &ConfigurationSettingLoader{} + _, err := loader.ProcessFeatureFlags(nil, []azappconfig.FeatureFlag{featureFlag}) + if err == nil { + t.Fatal("expected enhanced feature flag conversion to fail") + } + + if !strings.Contains(err.Error(), "Enhanced feature flag 'BrokenFlag':") { + t.Errorf("expected error to include the enhanced feature flag name, got %q", err) + } + if !strings.Contains(err.Error(), `failed to parse variant "BrokenVariant" value`) { + t.Errorf("expected error to include the conversion failure, got %q", err) + } +} + +func TestConvertFeatureFlagVariantValueFallsBackToString(t *testing.T) { + featureFlagName := "StringFlag" + variantName := "StringVariant" + value := "{not-json" + textContentType := "text/plain" + + for _, contentType := range []*string{nil, &textContentType} { + featureFlag := azappconfig.FeatureFlag{ + Name: &featureFlagName, + Variants: []azappconfig.FeatureFlagVariantDefinition{ + {Name: &variantName, Value: &value, ContentType: contentType}, + }, + } + + converted, err := convertToMicrosoftSchema(featureFlag) + if err != nil { + t.Fatalf("expected non-JSON variant value to remain a string: %s", err) + } + + variants := converted[featureFlagVariantsKey].([]interface{}) + variant := variants[0].(map[string]interface{}) + if variant[featureFlagConfigurationValueKey] != value { + t.Errorf("expected configuration value %q, got %#v", value, variant[featureFlagConfigurationValueKey]) + } + } +} + func TestEnhancedFeatureFlagSettingsClientLoadsEnhancedFlags(t *testing.T) { endpointNameFilter := "*" nullLabel := "\x00" From 3092b8964712cb733e1df1d9a40fbafc861b2a1e Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Mon, 31 Aug 2026 11:10:33 +0800 Subject: [PATCH 5/6] dependency update --- go.mod | 26 +++++++------- go.sum | 109 +++++++++++++++------------------------------------------ 2 files changed, 41 insertions(+), 94 deletions(-) diff --git a/go.mod b/go.mod index 2ac6cfe..78b6063 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,13 @@ module azappconfig/provider go 1.26.0 require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 - github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2 v2.2.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 + github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2 v2.2.1-beta.1 github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.5.0 github.com/golang/mock v1.6.0 github.com/onsi/gomega v1.39.1 - golang.org/x/crypto v0.53.0 - golang.org/x/sync v0.21.0 + golang.org/x/crypto v0.55.0 + golang.org/x/sync v0.22.0 k8s.io/apimachinery v0.36.2 k8s.io/client-go v0.36.2 sigs.k8s.io/controller-runtime v0.24.1 @@ -18,7 +18,7 @@ require ( require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect @@ -29,7 +29,6 @@ require ( github.com/go-openapi/swag/cmdutils v0.27.0 // indirect github.com/go-openapi/swag/conv v0.27.0 // indirect github.com/go-openapi/swag/fileutils v0.27.0 // indirect - github.com/go-openapi/swag/jsonname v0.27.0 // indirect github.com/go-openapi/swag/jsonutils v0.27.0 // indirect github.com/go-openapi/swag/loading v0.27.0 // indirect github.com/go-openapi/swag/mangling v0.27.0 // indirect @@ -39,7 +38,6 @@ require ( github.com/go-openapi/swag/yamlutils v0.27.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect - github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef // indirect github.com/kylelemons/godebug v1.1.0 // indirect @@ -49,8 +47,8 @@ require ( github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.36.0 // indirect - golang.org/x/tools v0.45.0 // indirect + golang.org/x/mod v0.38.0 // indirect + golang.org/x/tools v0.48.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.4.0 // indirect @@ -76,14 +74,14 @@ require ( github.com/prometheus/common v0.69.0 // indirect github.com/prometheus/procfs v0.21.1 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/stretchr/testify v1.11.1 + github.com/stretchr/testify v1.12.0 go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect - golang.org/x/net v0.56.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.41.0 // indirect golang.org/x/time v0.15.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect diff --git a/go.sum b/go.sum index 84493f9..2c340b5 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,11 @@ -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 h1:4gRPBpN1f6xt88yi4WR26m7XaD9OlWtVT6bWPdGUIok= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0/go.mod h1:G7QVLxw1j1JVyrO1MA95S8m8HStaaleDZYTcfGgjB2o= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 h1:CU4+EJeJi3TKYWEcYuSdWsjzw0nVsK/H0MSQOiPcymU= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0/go.mod h1:q0+UTSRvShwUCrR/s5HtyInYphN7Wvxb7snFM3u+SLA= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0/go.mod h1:mCBhUhlMjLLJKr5aqw2TNS/VqJOie8MzWq3DAMJeKso= -github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2 v2.2.0 h1:80ijeACessBdODMsTjR0zf7t1Uvhc+hCqcXc0BcW9ZA= -github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2 v2.2.0/go.mod h1:RPzAQTYyoren3gG5K0QSiWTSUhGCB47LY//1MGdODv0= +github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2 v2.2.1-beta.1 h1:4YCkgZ9q0utG+cGzfS2E/K9D8gO1enbZ8GrWJP1ajXU= +github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2 v2.2.1-beta.1/go.mod h1:dqVG0OgGgwlNRc40Y0GDIM4YPMzVpNZK3TvD1u6tpbw= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.5.0 h1:aMFOzch6ZJo4Ct9hI4A9Y2fPen5YNRTPmkSBhe5m0ZQ= @@ -14,8 +14,8 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfg github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU= -github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 h1:Nljr4q1GRA/5vCrMONS+g4u4LRHNgOXVSh3O43J2CnI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0/go.mod h1:Y33QHnf0FfdVewFFISOGe20mkZbxX4H839o955/PoeI= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -46,69 +46,38 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= -github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= github.com/go-openapi/jsonpointer v0.24.0 h1:AA6mCjHYHmZ+1RU2Js089EaOK/iwXXNwQsTgnsTha2M= github.com/go-openapi/jsonpointer v0.24.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= -github.com/go-openapi/swag v0.26.1 h1:l5sVEyVpwj+DDYeZyo7wQI/Ebn/mKYIyGB/pFwAfGoQ= -github.com/go-openapi/swag v0.26.1/go.mod h1:yNY38BbIVthxbkDtq1UHBCGasBqjakW3lCR6ANzdBEw= github.com/go-openapi/swag v0.27.0 h1:8ecSuZlh4NXc3GsmAOqECIYqDTApCWaMe3gO4gjJNEE= github.com/go-openapi/swag v0.27.0/go.mod h1:Kkgz9Ht0+ul9/aVdFmc9xSyPzUwf/aFF5KiFPBXfSY0= -github.com/go-openapi/swag/cmdutils v0.26.1 h1:f2iE1ijYaJ3nuu5PaEMx3zpEhzhZFgivCJObWEObLIQ= -github.com/go-openapi/swag/cmdutils v0.26.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= github.com/go-openapi/swag/cmdutils v0.27.0 h1:aIKiqhB29AaP+7xm8/CPg3uOpeHx2SUp6TvMpu/a31Y= github.com/go-openapi/swag/cmdutils v0.27.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= -github.com/go-openapi/swag/conv v0.26.1 h1:slr5FVkg9Wc3Y5zcwenD8Sd/PQ94b2I/QJI7N7KTBpg= -github.com/go-openapi/swag/conv v0.26.1/go.mod h1:mvQXgPptZk9GTrFgGwWvT4q+dN+zQej9JfmGwnipz1A= github.com/go-openapi/swag/conv v0.27.0 h1:EKOH4feXrvdo8DbSsXSAqRT8fz1epEnS5O2IfXUOzE8= github.com/go-openapi/swag/conv v0.27.0/go.mod h1:pfiv0uKQTbaGApk8Zs/lZV3uSjmSpa2FO1y183YngN8= -github.com/go-openapi/swag/fileutils v0.26.1 h1:K1XCM2CGhfNsc6YDt6v7Q5+1e59rftYWdcu/isZhvFw= -github.com/go-openapi/swag/fileutils v0.26.1/go.mod h1:mYUgxQAKX4ShS3qvvySx+/9yrlUnDhjiD1CalaQl8lQ= github.com/go-openapi/swag/fileutils v0.27.0 h1:ib5jMUqGq5tY1EyO4inlrabsaeDAleFU+XD1FXQcgp8= github.com/go-openapi/swag/fileutils v0.27.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= -github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE= -github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc= -github.com/go-openapi/swag/jsonname v0.27.0 h1:4QVB//CKOdE8IOiBg19JNY2wfDS48MhesIquYBy2rUE= -github.com/go-openapi/swag/jsonname v0.27.0/go.mod h1:I1YsyvvhBuZsFXSW6I7ODfdyq13p7hDil//1T9/pFFk= -github.com/go-openapi/swag/jsonutils v0.26.1 h1:2hdBfFkHg+7Wrz2VsCbeyR6hzkRDs7AztnMR2u84yOY= -github.com/go-openapi/swag/jsonutils v0.26.1/go.mod h1:U+RMJH3wa+6BRiphuRtIyI8fW9HPFqFQ4sHk2oRx0UQ= github.com/go-openapi/swag/jsonutils v0.27.0 h1:VYtd9jEQYeU4j8q5vdn5KWotF4vKywhGdMBrALtAsfE= github.com/go-openapi/swag/jsonutils v0.27.0/go.mod h1:U7pb8AGuwhok3RDicHeHwSG4L3PXSq6PAL98Aon632g= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1 h1:1CD7NiLLb/TXl3tOnFYU4b+mNfb5rtgHkaA+q7RMYYQ= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1/go.mod h1:ZWafc8nMdYzTE3uYY6W86f0n46+IF0g4uUyRhJw/kXc= github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0 h1:+d7C7Ur/SsGg/UZ9G0JEovnfRqtMNZCJQGKc2h/ojoE= -github.com/go-openapi/swag/loading v0.26.1 h1:E9K4wqXeROlhjFQ13K9zMz6ojFGXIggGe+ad1odrK9w= -github.com/go-openapi/swag/loading v0.26.1/go.mod h1:3qvRIlWzWdq1HvmldwmuJ2ohpcAryN6xVt2OTKd0/7E= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= github.com/go-openapi/swag/loading v0.27.0 h1:s8DA9aPEdFH6OluHUYUn3DnIuoTdyWs9RwffXBUfyeI= github.com/go-openapi/swag/loading v0.27.0/go.mod h1:VOz+Jg6UGGywcmRvYsI4fvtp+bd7NfioseGEPleYdA4= -github.com/go-openapi/swag/mangling v0.26.1 h1:gpYI4WuPKFJJVjV5cDLGlDVJhFIxYjQc7yN5eEb4CqM= -github.com/go-openapi/swag/mangling v0.26.1/go.mod h1:POETDH01hqAdASXfw7ISEd9bCOE6xBHOt8NHmGZRmYM= github.com/go-openapi/swag/mangling v0.27.0 h1:rpPJuqQHa6z2pDiP3iIpXOyNXlSs9cQCxnJSAxzdfOc= github.com/go-openapi/swag/mangling v0.27.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= -github.com/go-openapi/swag/netutils v0.26.1 h1:BNctoc39WTAUMxyAs355fExOPzMZtPbZ0ZZ1Am2FR5M= -github.com/go-openapi/swag/netutils v0.26.1/go.mod h1:y02vByhZhQPAVwOX+0KipXFZ/hUbk6G/Enhf5rGaOkQ= github.com/go-openapi/swag/netutils v0.27.0 h1:lEUG+hHvPvLggB3A8snFk0IRKNf9uC0YKc+7WYqvAF8= github.com/go-openapi/swag/netutils v0.27.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= -github.com/go-openapi/swag/stringutils v0.26.1 h1:f88uYyTso7TnHrKM/bUBsQ5e2wKf37cpgo6pvbzd9yU= -github.com/go-openapi/swag/stringutils v0.26.1/go.mod h1:Sc6d3bU8fgk5AyZR8/8jEQ+Is/Ald+TD/IIggPN8UJk= github.com/go-openapi/swag/stringutils v0.27.0 h1:Of7w/HljWsNZvuxsUAnw3n+hCOyI6HLJOxW2kQRAxio= github.com/go-openapi/swag/stringutils v0.27.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= -github.com/go-openapi/swag/typeutils v0.26.1 h1:yg42FgMzRR6PVQ3M3qHz1s+Y6/P4HoJ3cBarXa3OVnU= -github.com/go-openapi/swag/typeutils v0.26.1/go.mod h1:VfnV+oUtSP2vCSCn2aJgnr8OevUYemyIzzS1VOzS10o= github.com/go-openapi/swag/typeutils v0.27.0 h1:aCf4MSGo8NLwZP8Q6t32DWLJSvl/WwNqgmEG+xJ6v2o= github.com/go-openapi/swag/typeutils v0.27.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= -github.com/go-openapi/swag/yamlutils v0.26.1 h1:0TSLK+lXs9vfIhAWzBeI/lOzEnIoot6WTCO1aAeWFTk= -github.com/go-openapi/swag/yamlutils v0.26.1/go.mod h1:7W5b7PRX9MxwL7TjeG7H8HkyBGRsIDRObhyMWFgBI2M= github.com/go-openapi/swag/yamlutils v0.27.0 h1:bQ6eAMil5X9tdcf7dMn4t15alzG6jddnrKPuKa/zxKM= github.com/go-openapi/swag/yamlutils v0.27.0/go.mod h1:yRfIo7qqVkmJRQjX8exjA3AfcI8rH1KDNPsTparoCv4= -github.com/go-openapi/testify/enable/yaml/v2 v2.5.1 h1:q9NtHwK4qHF7yZziBPvZyv7zWAIk8ok88Gh2mR6Jpc8= -github.com/go-openapi/testify/enable/yaml/v2 v2.5.1/go.mod h1:JW0MXIotCYps/XsgJnG3a8Q7rE5xAiBwoOD5OfaIQBk= github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= -github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0an6sBo= -github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= +github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= @@ -117,8 +86,6 @@ github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63Y github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= -github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -171,12 +138,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY= -github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk= github.com/prometheus/common v0.69.0/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= -github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= -github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -184,11 +147,11 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= +github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -212,44 +175,44 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -268,34 +231,20 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.35.5 h1:BrFeUDGY/LBtlA1R5RoxhlYRHs76RnQBc6xbm/y7hsQ= -k8s.io/api v0.35.5/go.mod h1:xWkFhMnoPZdTAQh95Rlw3zZpUUNVlFHcuESUYd06BWM= k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY= k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= -k8s.io/apiextensions-apiserver v0.35.5 h1:HttlJjgsx3ddLsASCqklkKvfBlwUoXma8VLpeMG5YL8= -k8s.io/apiextensions-apiserver v0.35.5/go.mod h1:4xbAgP/jbt8sVHE3H4DfE1gSPLUoSzXrNqhZz1lTHKc= k8s.io/apiextensions-apiserver v0.36.2 h1:3O5gqOj/dt2XWWbpMe+TXWpE9yU6pjM/tXxtHHJT/K4= k8s.io/apiextensions-apiserver v0.36.2/go.mod h1:cL1tBWe8XSaP1H30iWKGo7hf6iAUUUJPEU70dskmAnA= -k8s.io/apimachinery v0.35.5 h1:lbjjjUfVeVqFbiOpyhqZHc8DhiYkWOxSNij7lHx2U8Y= -k8s.io/apimachinery v0.35.5/go.mod h1:NNi1taPOpep0jOj+oRha3mBJPqvi0hGdaV8TCqGQ+cc= k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ= k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= -k8s.io/client-go v0.35.5 h1:wUrgqVSmFRw75bgSHY7X0G/hZM/QYpV0Hg7SYYOYpFk= -k8s.io/client-go v0.35.5/go.mod h1:Z0mDcAJsX1Y7RQfuQlJipiRtqf8Mhk2VDu1/JvRqdGo= k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI= k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= -k8s.io/kube-openapi v0.0.0-20260520065146-aa012df4f4af h1:zLXA2Irn14q2/06WMkxViyr7YCPUO2lJ0QYE9Juy5vA= -k8s.io/kube-openapi v0.0.0-20260520065146-aa012df4f4af/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY= k8s.io/kube-openapi v0.0.0-20260624041617-8f3fa4921821 h1:m2wZhD5+vJZyCVkTvUHIfaiXc/mdt3Pxyx3vUnGsKzU= k8s.io/kube-openapi v0.0.0-20260624041617-8f3fa4921821/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY= -k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7zC6CPF+C79jroc= -k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs= k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= -sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= -sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= From 2e4d3f64f314a731dd97c945d72dbe917e1e2e6b Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Tue, 1 Sep 2026 07:50:15 +0800 Subject: [PATCH 6/6] dependency upgrade --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 78b6063..8524acb 100644 --- a/go.mod +++ b/go.mod @@ -95,5 +95,5 @@ require ( k8s.io/utils v0.0.0-20260626114624-be93311217bd // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/yaml v1.6.0 // indirect - software.sslmate.com/src/go-pkcs12 v0.7.0 + software.sslmate.com/src/go-pkcs12 v0.7.3 ) diff --git a/go.sum b/go.sum index 2c340b5..c24fe5c 100644 --- a/go.sum +++ b/go.sum @@ -255,5 +255,5 @@ sigs.k8s.io/structured-merge-diff/v6 v6.4.0 h1:qmp2e3ZfFi1/jJbDGpD4mt3wyp6PE1NfK sigs.k8s.io/structured-merge-diff/v6 v6.4.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= -software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0= -software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +software.sslmate.com/src/go-pkcs12 v0.7.3 h1:JBQD3FDqYjTeyDAeZQklj2ar88ykBLtALloPJHyAauU= +software.sslmate.com/src/go-pkcs12 v0.7.3/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=