From 84f574447a41f391902e1fd97f1eabbfbbcb6207 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 6 Feb 2023 12:02:28 -0800 Subject: [PATCH 01/38] Moving Spring Provider to the Spring folder --- .../pom.xml | 100 ---- .../config/web/pullrefresh/package-info.java | 6 - .../web/pushbusrefresh/package-info.java | 6 - .../config/web/pushrefresh/package-info.java | 6 - .../pom.xml | 168 ------ ...ppConfigurationBootstrapConfiguration.java | 117 ----- .../AppConfigurationCredentialProvider.java | 20 - ...pConfigurationHealthAutoConfiguration.java | 31 -- .../config/KeyVaultCredentialProvider.java | 19 - .../cloud/config/TokenCredentialProvider.java | 28 - .../management/entity/package-info.java | 6 - .../AppConfigurationHealthIndicator.java | 47 -- .../cloud/config/health/package-info.java | 6 - .../AppConfigurationPropertySource.java | 336 ------------ ...AppConfigurationReplicaClientsBuilder.java | 243 --------- .../config/pipline/policies/package-info.java | 6 - .../config/properties/FeatureFlagStore.java | 57 --- .../cloud/config/properties/package-info.java | 6 - .../AppConfigManagedIdentityProperties.java | 31 -- .../cloud/config/resource/package-info.java | 6 - .../cloud/config/stores/KeyVaultClient.java | 124 ----- .../cloud/config/stores/package-info.java | 6 - .../main/resources/META-INF/spring.factories | 6 - .../AppConfigurationHealthIndicatorTest.java | 111 ---- .../AppConfigurationPropertySourceTest.java | 466 ----------------- .../config/stores/KeyVaultClientTest.java | 209 -------- .../main/resources/META-INF/spring.factories | 2 - .../README.md | 277 ---------- .../FeatureManagementConfiguration.java | 26 - .../cloud/feature/manager/FeatureManager.java | 195 ------- .../feature/filters/AlwaysOnFilter.java | 16 - .../feature/manager/FeatureManagerTest.java | 268 ---------- sdk/appconfiguration/ci.yml | 84 +-- sdk/appconfiguration/pom.xml | 16 +- sdk/appconfiguration/tests.yml | 10 +- sdk/spring/ci.yml | 40 ++ sdk/spring/pom.xml | 16 +- .../CHANGELOG.md | 2 +- .../README.md | 0 .../pom.xml | 195 +++++++ .../AppConfigurationWebAutoConfiguration.java | 24 +- .../AppConfigurationEndpoint.java | 22 +- .../AppConfigurationWebConstants.java | 2 +- .../AppConfigurationEventListener.java | 8 +- .../AppConfigurationBusRefreshEndpoint.java | 10 +- .../AppConfigurationBusRefreshEvent.java | 2 +- ...pConfigurationBusRefreshEventListener.java | 2 +- .../AppConfigurationRefreshEndpoint.java | 10 +- .../AppConfigurationRefreshEvent.java | 2 +- .../AppConfigurationRefreshEventListener.java | 2 +- .../spring/cloud/config/web/package-info.java | 2 +- .../main/resources/META-INF/spring.factories | 0 ...ConfigurationWebAutoConfigurationTest.java | 20 +- .../AppConfigurationEndpointTest.java | 4 +- .../web/implementation}/TestConstants.java | 2 +- .../config/web/implementation}/TestUtils.java | 18 +- .../AppConfigurationEventListenerTest.java | 2 +- ...ppConfigurationBusRefreshEndpointTest.java | 22 +- .../AppConfigurationRefreshEndpointTest.java | 22 +- .../org.mockito.plugins.MockMaker | 0 .../src/test/resources/webHookInvalid.json | 0 .../src/test/resources/webHookRefresh.json | 0 .../src/test/resources/webHookValidation.json | 0 .../CHANGELOG.md | 4 +- .../README.md | 0 .../pom.xml | 181 +++++++ .../cloud/config/AppConfigurationRefresh.java | 2 +- .../ConfigurationClientCustomizer.java} | 2 +- .../cloud/config/KeyVaultSecretProvider.java | 6 +- .../cloud/config/SecretClientCustomizer.java} | 8 +- ...ationApplicationSettingPropertySource.java | 127 +++++ .../AppConfigurationConstants.java | 33 +- ...rationFeatureManagementPropertySource.java | 188 +++++++ ...AppConfigurationKeyVaultClientFactory.java | 52 ++ .../AppConfigurationPropertySource.java | 60 +++ ...AppConfigurationPropertySourceLocator.java | 173 +++---- .../AppConfigurationPullRefresh.java | 29 +- .../AppConfigurationRefreshUtil.java | 191 ++++--- .../AppConfigurationReplicaClient.java | 19 +- .../AppConfigurationReplicaClientFactory.java | 18 +- ...AppConfigurationReplicaClientsBuilder.java | 255 +++++++++ .../AppConfigurationStatusException.java | 2 +- .../implementation/BackoffTimeCalculator.java | 15 +- .../implementation/ConnectionManager.java | 19 +- .../config/implementation}/HostType.java | 2 +- .../JsonConfigurationParser.java | 4 +- .../config/implementation}/NormalizeNull.java | 2 +- .../RequestTracingConstants.java | 2 +- .../config/implementation}/RequestType.java | 2 +- .../cloud/config/implementation/State.java | 0 .../config/implementation/StateHolder.java | 30 +- .../AppConfigurationAutoConfiguration.java | 9 +- ...ppConfigurationBootstrapConfiguration.java | 187 +++++++ .../feature/management/entity/Feature.java | 2 +- .../feature/management/entity/FeatureSet.java | 2 +- .../health/AppConfigurationStoreHealth.java | 2 +- .../policies/BaseAppConfigurationPolicy.java | 14 +- .../AppConfigurationKeyValueSelector.java | 109 ++++ .../AppConfigurationProperties.java | 97 +--- .../AppConfigurationProviderProperties.java | 7 +- .../AppConfigurationStoreMonitoring.java | 2 +- .../AppConfigurationStoreTrigger.java | 13 +- .../properties/ConfigStore.java | 15 +- .../FeatureFlagKeyValueSelector.java} | 26 +- .../properties/FeatureFlagStore.java | 58 +++ .../AppConfigurationSecretClientManager.java | 97 ++++ .../spring/cloud/config/package-info.java | 0 .../main/resources/META-INF/spring.factories | 6 + .../src/main/resources/appConfiguration.yaml | 0 ...nApplicationSettingPropertySourceTest.java | 159 ++++++ ...onFeatureManagementPropertySourceTest.java | 227 ++++++++ ...nfigurationPropertySourceKeyVaultTest.java | 98 ++-- ...onfigurationPropertySourceLocatorTest.java | 253 ++++++--- .../AppConfigurationPullRefreshTest.java | 15 +- .../AppConfigurationRefreshUtilTest.java | 170 +++--- ...ConfigurationReplicaClientBuilderTest.java | 119 ++--- ...ConfigurationReplicaClientFactoryTest.java | 25 +- .../AppConfigurationReplicaClientTest.java | 63 ++- .../BackoffTimeCalculatorTest.java | 1 - .../implementation/ConnectionManagerTest.java | 44 +- .../JsonConfigurationParserTest.java | 0 .../implementation/StateHolderTest.java | 19 +- .../config/implementation}/TestConstants.java | 2 +- .../config/implementation/TestUtils.java | 27 +- ...nfigurationBootstrapConfigurationTest.java | 22 +- .../BaseAppConfigurationPolicyTest.java | 10 +- .../AppConfigurationPropertiesTest.java | 55 +- .../AppConfigurationStoreMonitoringTest.java | 54 ++ .../AppConfigurationStoreSelectsTest.java | 22 +- .../FeatureFlagKeyValueSelectorTest.java | 75 +++ .../properties/FeatureFlagStoreTest.java | 45 ++ .../stores/ConfigStoreTest.java | 30 +- .../stores/KeyVaultClientTest.java | 130 +++++ .../test/resources/jsonContentTypeData.json | 0 .../org.mockito.plugins.MockMaker | 0 .../CHANGELOG.md | 17 +- .../README.md | 2 +- .../pom.xml | 45 +- .../DynamicFeatureManagerSnapshot.java | 59 +++ .../cloud/feature/manager/FeatureGate.java | 0 .../cloud/feature/manager/FeatureHandler.java | 6 +- .../manager/FeatureManagerSnapshot.java | 7 +- .../manager/IDisabledFeaturesHandler.java | 2 +- .../implementation}/FeatureConfig.java | 4 +- .../FeatureManagementWebConfiguration.java | 26 +- .../manager/implementation/package-info.java | 3 + .../cloud/feature/manager/package-info.java | 7 + .../main/resources/META-INF/spring.factories | 2 + .../DynamicFeatureManagerSnapshotTest.java | 80 +++ .../feature/manager/FeatureHandlerTest.java | 42 +- .../manager/FeatureManagerSnapshotTest.java | 8 +- .../CHANGELOG.md | 17 +- .../README.md | 484 ++++++++++++++++++ .../pom.xml | 45 +- .../manager/DynamicFeatureException.java | 31 ++ .../manager/DynamicFeatureManager.java | 170 ++++++ .../FeatureManagementConfiguration.java | 50 ++ .../manager/FeatureManagementException.java | 44 ++ .../cloud/feature/manager/FeatureManager.java | 131 +++++ .../manager/FilterNotFoundException.java | 10 +- .../feature/manager/FilterParameters.java | 6 +- .../manager/IDynamicFeatureProperties.java | 10 + .../feature/manager/TargetingException.java | 4 +- .../feature/filters/AlwaysOnFilter.java | 18 + .../feature/filters/PercentageFilter.java | 8 +- .../feature/filters/TargetingEvaluator.java | 195 +++++++ .../feature/filters/TargetingFilter.java | 126 +++-- .../feature/filters/TimeWindowFilter.java | 6 +- .../manager/feature/filters/package-info.java | 8 + .../FeatureManagementConfigProperties.java | 4 +- .../FeatureManagementProperties.java | 151 ++++++ .../implementation/models/DynamicFeature.java | 48 ++ .../implementation/models}/Feature.java | 3 +- .../implementation/models/package-info.java | 6 + .../manager/implementation/package-info.java | 8 + .../implementation}/targeting/Audience.java | 2 +- .../targeting/GroupRollout.java | 2 +- .../targeting/TargetingFilterSettings.java | 2 +- .../targeting/package-info.java | 1 + .../manager/models/FeatureDefinition.java | 60 +++ .../FeatureFilterEvaluationContext.java | 5 +- .../manager/models/FeatureVariant.java | 85 +++ .../manager/models/IFeatureFilter.java} | 6 +- .../models/IFeatureVariantAssigner.java | 20 + .../feature/manager/models/package-info.java | 6 + .../cloud/feature/manager/package-info.java | 6 + .../manager/targeting/ITargetingContext.java | 0 .../targeting/ITargetingContextAccessor.java | 0 .../manager/targeting/TargetingContext.java | 2 +- .../targeting/TargetingEvaluationOptions.java | 2 +- .../manager/targeting/package-info.java | 6 + .../main/resources/META-INF/spring.factories | 0 .../manager/DynamicFeatureManagerTest.java | 272 ++++++++++ .../feature/manager/FeatureManagerTest.java | 172 +++++++ .../cloud/feature/manager/SpringBootTest.java | 17 +- .../feature/manager/TestConfiguration.java | 4 + .../feature/filters/PercentageFilterTest.java | 2 +- .../filters/TargetingEvaluatorTest.java | 219 ++++++++ .../feature/filters/TargetingFilterTest.java | 2 +- .../feature/filters/TimeWindowFilterTest.java | 2 +- .../FeatureManagementPropertiesTest.java | 160 ++++++ .../manager/testobjects/BasicObject.java | 23 + .../manager/testobjects/DiscountBanner.java | 33 ++ .../testobjects/MockableProperties.java | 27 + .../src/test/resources/application.yaml | 0 .../CHANGELOG.md | 8 +- .../README.md | 4 +- .../pom.xml | 6 +- .../CHANGELOG.md | 0 .../README.md | 0 .../pom.xml | 6 +- .../spring/cloud/config/AppConfiguration.java | 0 .../spring/cloud/config/LoadConfigsTest.java | 0 .../cloud/config/MessageProperties.java | 0 .../spring/cloud/config/MyCredentials.java | 0 .../test/resources/META-INF/spring.factories | 0 .../test-resources.json | 0 217 files changed, 6176 insertions(+), 4106 deletions(-) delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/pom.xml delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pullrefresh/package-info.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushbusrefresh/package-info.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushrefresh/package-info.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/pom.xml delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationBootstrapConfiguration.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationCredentialProvider.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationHealthAutoConfiguration.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/KeyVaultCredentialProvider.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/TokenCredentialProvider.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/feature/management/entity/package-info.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/health/AppConfigurationHealthIndicator.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/health/package-info.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySource.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/pipline/policies/package-info.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/FeatureFlagStore.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/package-info.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/resource/AppConfigManagedIdentityProperties.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/resource/package-info.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/stores/KeyVaultClient.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/stores/package-info.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/resources/META-INF/spring.factories delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationHealthIndicatorTest.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceTest.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/stores/KeyVaultClientTest.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/resources/META-INF/spring.factories delete mode 100644 sdk/appconfiguration/azure-spring-cloud-feature-management/README.md delete mode 100644 sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManager.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/AlwaysOnFilter.java delete mode 100644 sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/FeatureManagerTest.java rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web => spring/spring-cloud-azure-appconfiguration-config-web}/CHANGELOG.md (99%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web => spring/spring-cloud-azure-appconfiguration-config-web}/README.md (100%) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web => spring/spring-cloud-azure-appconfiguration-config-web}/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java (79%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web => spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation}/AppConfigurationEndpoint.java (86%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web => spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation}/AppConfigurationWebConstants.java (95%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web => spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation}/pullrefresh/AppConfigurationEventListener.java (79%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web => spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation}/pushbusrefresh/AppConfigurationBusRefreshEndpoint.java (89%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web => spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation}/pushbusrefresh/AppConfigurationBusRefreshEvent.java (96%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web => spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation}/pushbusrefresh/AppConfigurationBusRefreshEventListener.java (95%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web => spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation}/pushrefresh/AppConfigurationRefreshEndpoint.java (89%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web => spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation}/pushrefresh/AppConfigurationRefreshEvent.java (94%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web => spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation}/pushrefresh/AppConfigurationRefreshEventListener.java (95%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web => spring/spring-cloud-azure-appconfiguration-config-web}/src/main/java/com/azure/spring/cloud/config/web/package-info.java (62%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web => spring/spring-cloud-azure-appconfiguration-config-web}/src/main/resources/META-INF/spring.factories (100%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web => spring/spring-cloud-azure-appconfiguration-config-web}/src/test/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfigurationTest.java (80%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web => spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation}/AppConfigurationEndpointTest.java (98%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web => spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation}/TestConstants.java (96%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web => spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation}/TestUtils.java (70%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web => spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation}/pullrefresh/AppConfigurationEventListenerTest.java (95%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web => spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation}/pushbusrefresh/AppConfigurationBusRefreshEndpointTest.java (91%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web => spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation}/pushrefresh/AppConfigurationRefreshEndpointTest.java (91%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web => spring/spring-cloud-azure-appconfiguration-config-web}/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker (100%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web => spring/spring-cloud-azure-appconfiguration-config-web}/src/test/resources/webHookInvalid.json (100%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web => spring/spring-cloud-azure-appconfiguration-config-web}/src/test/resources/webHookRefresh.json (100%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config-web => spring/spring-cloud-azure-appconfiguration-config-web}/src/test/resources/webHookValidation.json (100%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/CHANGELOG.md (98%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/README.md (100%) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/main/java/com/azure/spring/cloud/config/AppConfigurationRefresh.java (94%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/ConfigurationClientBuilderSetup.java => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/ConfigurationClientCustomizer.java} (91%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/main/java/com/azure/spring/cloud/config/KeyVaultSecretProvider.java (73%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/SecretClientBuilderSetup.java => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/SecretClientCustomizer.java} (73%) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationApplicationSettingPropertySource.java rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation}/AppConfigurationConstants.java (72%) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySource.java create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationKeyVaultClientFactory.java create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySource.java rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocator.java (61%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefresh.java (83%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtil.java (68%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClient.java (92%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactory.java (88%) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationStatusException.java (99%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/main/java/com/azure/spring/cloud/config/implementation/BackoffTimeCalculator.java (82%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/main/java/com/azure/spring/cloud/config/implementation/ConnectionManager.java (88%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation}/HostType.java (93%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/main/java/com/azure/spring/cloud/config/implementation/JsonConfigurationParser.java (96%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation}/NormalizeNull.java (93%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation}/RequestTracingConstants.java (96%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation}/RequestType.java (92%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/main/java/com/azure/spring/cloud/config/implementation/State.java (100%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/main/java/com/azure/spring/cloud/config/implementation/StateHolder.java (86%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation}/config/AppConfigurationAutoConfiguration.java (80%) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation}/feature/management/entity/Feature.java (95%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation}/feature/management/entity/FeatureSet.java (93%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation}/health/AppConfigurationStoreHealth.java (87%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation}/pipline/policies/BaseAppConfigurationPolicy.java (89%) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationKeyValueSelector.java rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation}/properties/AppConfigurationProperties.java (60%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation}/properties/AppConfigurationProviderProperties.java (95%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation}/properties/AppConfigurationStoreMonitoring.java (98%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation}/properties/AppConfigurationStoreTrigger.java (80%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation}/properties/ConfigStore.java (90%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationStoreSelects.java => spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagKeyValueSelector.java} (78%) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagStore.java create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/stores/AppConfigurationSecretClientManager.java rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/main/java/com/azure/spring/cloud/config/package-info.java (100%) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/main/resources/appConfiguration.yaml (100%) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java (52%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocatorTest.java (60%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefreshTest.java (86%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtilTest.java (81%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientBuilderTest.java (58%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactoryTest.java (72%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientTest.java (65%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/implementation/BackoffTimeCalculatorTest.java (99%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/implementation/ConnectionManagerTest.java (77%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/implementation/JsonConfigurationParserTest.java (100%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/implementation/StateHolderTest.java (92%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation}/TestConstants.java (98%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/implementation/TestUtils.java (80%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud => spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation}/config/AppConfigurationBootstrapConfigurationTest.java (68%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation}/pipline/policies/BaseAppConfigurationPolicyTest.java (91%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties}/AppConfigurationPropertiesTest.java (65%) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationStoreMonitoringTest.java rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation}/properties/AppConfigurationStoreSelectsTest.java (74%) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagKeyValueSelectorTest.java create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagStoreTest.java rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config => spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation}/stores/ConfigStoreTest.java (59%) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/stores/KeyVaultClientTest.java rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/test/resources/jsonContentTypeData.json (100%) rename sdk/{appconfiguration/azure-spring-cloud-appconfiguration-config => spring/spring-cloud-azure-appconfiguration-config}/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker (100%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management-web => spring/spring-cloud-azure-feature-management-web}/CHANGELOG.md (88%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management-web => spring/spring-cloud-azure-feature-management-web}/README.md (51%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management-web => spring/spring-cloud-azure-feature-management-web}/pom.xml (65%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshot.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management-web => spring/spring-cloud-azure-feature-management-web}/src/main/java/com/azure/spring/cloud/feature/manager/FeatureGate.java (100%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management-web => spring/spring-cloud-azure-feature-management-web}/src/main/java/com/azure/spring/cloud/feature/manager/FeatureHandler.java (95%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management-web => spring/spring-cloud-azure-feature-management-web}/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagerSnapshot.java (90%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management-web => spring/spring-cloud-azure-feature-management-web}/src/main/java/com/azure/spring/cloud/feature/manager/IDisabledFeaturesHandler.java (95%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager => spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation}/FeatureConfig.java (88%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager => spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation}/FeatureManagementWebConfiguration.java (64%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/package-info.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management-web/src/main/resources/META-INF/spring.factories create mode 100644 sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshotTest.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management-web => spring/spring-cloud-azure-feature-management-web}/src/test/java/com/azure/spring/cloud/feature/manager/FeatureHandlerTest.java (88%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management-web => spring/spring-cloud-azure-feature-management-web}/src/test/java/com/azure/spring/cloud/feature/manager/FeatureManagerSnapshotTest.java (91%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/CHANGELOG.md (88%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/README.md rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/pom.xml (64%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureException.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManager.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementException.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManager.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/main/java/com/azure/spring/cloud/feature/manager/FilterNotFoundException.java (69%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/main/java/com/azure/spring/cloud/feature/manager/FilterParameters.java (87%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/IDynamicFeatureProperties.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/main/java/com/azure/spring/cloud/feature/manager/TargetingException.java (84%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/AlwaysOnFilter.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilter.java (84%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingEvaluator.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingFilter.java (64%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilter.java (92%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/package-info.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager => spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation}/FeatureManagementConfigProperties.java (89%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementProperties.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/DynamicFeature.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/entities => spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models}/Feature.java (90%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/package-info.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager => spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation}/targeting/Audience.java (94%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager => spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation}/targeting/GroupRollout.java (91%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager => spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation}/targeting/TargetingFilterSettings.java (87%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/targeting/package-info.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/FeatureDefinition.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/entities => spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models}/FeatureFilterEvaluationContext.java (91%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/FeatureVariant.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureFilter.java => spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/IFeatureFilter.java} (79%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/IFeatureVariantAssigner.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/package-info.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/package-info.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/main/java/com/azure/spring/cloud/feature/manager/targeting/ITargetingContext.java (100%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/main/java/com/azure/spring/cloud/feature/manager/targeting/ITargetingContextAccessor.java (100%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingContext.java (92%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingEvaluationOptions.java (93%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/package-info.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/main/resources/META-INF/spring.factories (100%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerTest.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/FeatureManagerTest.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/test/java/com/azure/spring/cloud/feature/manager/SpringBootTest.java (57%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/test/java/com/azure/spring/cloud/feature/manager/TestConfiguration.java (69%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilterTest.java (96%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingEvaluatorTest.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingFilterTest.java (99%) rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilterTest.java (97%) create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementPropertiesTest.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/BasicObject.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/DiscountBanner.java create mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/MockableProperties.java rename sdk/{appconfiguration/azure-spring-cloud-feature-management => spring/spring-cloud-azure-feature-management}/src/test/resources/application.yaml (100%) rename sdk/{appconfiguration/azure-spring-cloud-starter-appconfiguration-config => spring/spring-cloud-azure-starter-appconfiguration-config}/CHANGELOG.md (95%) rename sdk/{appconfiguration/azure-spring-cloud-starter-appconfiguration-config => spring/spring-cloud-azure-starter-appconfiguration-config}/README.md (99%) rename sdk/{appconfiguration/azure-spring-cloud-starter-appconfiguration-config => spring/spring-cloud-azure-starter-appconfiguration-config}/pom.xml (91%) rename sdk/{appconfiguration/azure-spring-cloud-test-appconfiguration-config => spring/spring-cloud-azure-test-appconfiguration-config}/CHANGELOG.md (100%) rename sdk/{appconfiguration/azure-spring-cloud-test-appconfiguration-config => spring/spring-cloud-azure-test-appconfiguration-config}/README.md (100%) rename sdk/{appconfiguration/azure-spring-cloud-test-appconfiguration-config => spring/spring-cloud-azure-test-appconfiguration-config}/pom.xml (91%) rename sdk/{appconfiguration/azure-spring-cloud-test-appconfiguration-config => spring/spring-cloud-azure-test-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java (100%) rename sdk/{appconfiguration/azure-spring-cloud-test-appconfiguration-config => spring/spring-cloud-azure-test-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java (100%) rename sdk/{appconfiguration/azure-spring-cloud-test-appconfiguration-config => spring/spring-cloud-azure-test-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/MessageProperties.java (100%) rename sdk/{appconfiguration/azure-spring-cloud-test-appconfiguration-config => spring/spring-cloud-azure-test-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/MyCredentials.java (100%) rename sdk/{appconfiguration/azure-spring-cloud-test-appconfiguration-config => spring/spring-cloud-azure-test-appconfiguration-config}/src/test/resources/META-INF/spring.factories (100%) rename sdk/{appconfiguration/azure-spring-cloud-test-appconfiguration-config => spring/spring-cloud-azure-test-appconfiguration-config}/test-resources.json (100%) diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/pom.xml b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/pom.xml deleted file mode 100644 index a260cfb2f8412..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/pom.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - com.azure - azure-client-sdk-parent - 1.7.0 - ../../parents/azure-client-sdk-parent - - 4.0.0 - - com.azure.spring - azure-spring-cloud-appconfiguration-config-web - 2.12.0-beta.1 - Azure Spring Cloud App Configuration Config Web - Integration of Spring Cloud Config and Azure App Configuration Service - - - false - - - - - - - com.azure.spring - azure-spring-cloud-appconfiguration-config - 2.12.0-beta.1 - - - org.springframework.boot - spring-boot-starter-web - 2.7.8 - - - org.springframework.boot - spring-boot-starter-actuator - 2.7.8 - true - - - org.springframework.cloud - spring-cloud-bus - 3.1.2 - true - - - org.junit.vintage - junit-vintage-engine - 5.9.1 - test - - - org.hamcrest - hamcrest-core - - - - - junit - junit - 4.13.2 - test - - - org.mockito - mockito-core - 4.5.1 - test - - - org.springframework.boot - spring-boot-starter-test - 2.7.8 - test - - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.0.0-M3 - - - - - org.springframework.boot:spring-boot-starter-actuator:[2.7.8] - org.springframework.boot:spring-boot-starter-web:[2.7.8] - org.springframework.cloud:spring-cloud-bus:[3.1.2] - - - - - - - - diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pullrefresh/package-info.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pullrefresh/package-info.java deleted file mode 100644 index dfab3be760bc5..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pullrefresh/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package contains classes for pull bus refresh requests. - */ -package com.azure.spring.cloud.config.web.pullrefresh; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushbusrefresh/package-info.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushbusrefresh/package-info.java deleted file mode 100644 index 5ab2a133144f1..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushbusrefresh/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package contains classes for push bus refresh requests. - */ -package com.azure.spring.cloud.config.web.pushbusrefresh; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushrefresh/package-info.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushrefresh/package-info.java deleted file mode 100644 index 5c9c7c260e9ba..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushrefresh/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package contains classes for push refresh requests. - */ -package com.azure.spring.cloud.config.web.pushrefresh; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/pom.xml b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/pom.xml deleted file mode 100644 index 5b768a3e94456..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/pom.xml +++ /dev/null @@ -1,168 +0,0 @@ - - - - com.azure - azure-client-sdk-parent - 1.7.0 - ../../parents/azure-client-sdk-parent - - 4.0.0 - - com.azure.spring - azure-spring-cloud-appconfiguration-config - 2.12.0-beta.1 - Azure Spring Cloud App Configuration Config - Integration of Spring Cloud Config and Azure App Configuration Service - - - false - - - - - - - org.springframework.boot - spring-boot-autoconfigure-processor - 2.7.8 - - - org.springframework.boot - spring-boot-autoconfigure - 2.7.8 - - - org.springframework.boot - spring-boot-configuration-processor - 2.7.8 - true - - - org.springframework.cloud - spring-cloud-starter-bootstrap - 3.1.5 - - - org.springframework.cloud - spring-cloud-context - 3.1.5 - - - com.fasterxml.jackson.core - jackson-annotations - 2.13.4 - - - com.fasterxml.jackson.core - jackson-databind - 2.13.4.2 - - - - com.azure - azure-core - 1.36.0 - - - com.azure - azure-data-appconfiguration - 1.4.1 - - - com.azure - azure-identity - 1.8.0 - - - com.azure - azure-security-keyvault-secrets - 4.5.3 - - - com.azure - azure-core-http-netty - 1.13.0 - - - org.hibernate.validator - hibernate-validator - 6.2.5.Final - - - org.springframework.boot - spring-boot-actuator-autoconfigure - 2.7.8 - - - - - org.springframework.boot - spring-boot-starter-test - 2.7.8 - test - - - - - com.google.code.findbugs - jsr305 - 3.0.2 - provided - - - javax.annotation - javax.annotation-api - 1.3.2 - - - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.0.0-M3 - - - - - com.fasterxml.jackson.core:jackson-annotations:[2.13.4] - com.fasterxml.jackson.core:jackson-databind:[2.13.4.2] - javax.annotation:javax.annotation-api:[1.3.2] - org.apache.commons:commons-lang3:[3.12.0] - org.apache.httpcomponents:httpclient:[4.5.14] - org.hibernate.validator:hibernate-validator:[6.2.5.Final] - org.springframework.boot:spring-boot-autoconfigure-processor:[2.7.8] - org.springframework.boot:spring-boot-autoconfigure:[2.7.8] - org.springframework.boot:spring-boot-actuator-autoconfigure:[2.7.8] - org.springframework.boot:spring-boot-configuration-processor:[2.7.8] - org.springframework.cloud:spring-cloud-context:[3.1.5] - org.springframework.cloud:spring-cloud-starter-bootstrap:[3.1.5] - org.springframework:spring-web:[5.3.25] - - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.1.2 - - - - true - true - - - - - - - diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationBootstrapConfiguration.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationBootstrapConfiguration.java deleted file mode 100644 index 7f5a750de657f..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationBootstrapConfiguration.java +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import com.azure.spring.cloud.config.implementation.AppConfigurationPropertySourceLocator; -import com.azure.spring.cloud.config.implementation.AppConfigurationReplicaClientFactory; -import com.azure.spring.cloud.config.implementation.AppConfigurationReplicaClientsBuilder; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationProviderProperties; - -/** - * Setup ConnectionPool, AppConfigurationPropertySourceLocator, and ClientStore when - * spring.cloud.azure.appconfiguration.enabled is enabled. - */ -@Configuration -@EnableConfigurationProperties({ AppConfigurationProperties.class, AppConfigurationProviderProperties.class }) -@ConditionalOnClass(AppConfigurationPropertySourceLocator.class) -@ConditionalOnProperty(prefix = AppConfigurationProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true) -public class AppConfigurationBootstrapConfiguration { - - @Autowired - private transient ApplicationContext context; - - /** - * - * @param properties Client properties - * @param appProperties Library properties - * @param clientFactory Store Connections - * - * @return AppConfigurationPropertySourceLocator - * @throws IllegalArgumentException if both KeyVaultClientProvider and KeyVaultSecretProvider exist. - */ - @Bean - AppConfigurationPropertySourceLocator sourceLocator(AppConfigurationProperties properties, - AppConfigurationProviderProperties appProperties, AppConfigurationReplicaClientFactory clientFactory) - throws IllegalArgumentException { - - KeyVaultCredentialProvider keyVaultCredentialProvider = context - .getBeanProvider(KeyVaultCredentialProvider.class).getIfAvailable(); - SecretClientBuilderSetup keyVaultClientProvider = context.getBeanProvider(SecretClientBuilderSetup.class) - .getIfAvailable(); - KeyVaultSecretProvider keyVaultSecretProvider = context.getBeanProvider(KeyVaultSecretProvider.class) - .getIfAvailable(); - - if (keyVaultClientProvider != null && keyVaultSecretProvider != null) { - throw new IllegalArgumentException( - "KeyVaultClientProvider and KeyVaultSecretProvider both can't have Beans supplied."); - } - - return new AppConfigurationPropertySourceLocator(properties, appProperties, clientFactory, - keyVaultCredentialProvider, keyVaultClientProvider, keyVaultSecretProvider); - } - - /** - * Factory for working with App Configuration Clients - * - * @param clientBuilder Builder for configuration clients - * @param properties Client configurations for setting up connections to each config store. - * @return AppConfigurationReplicaClientFactory - */ - @Bean - @ConditionalOnMissingBean - AppConfigurationReplicaClientFactory replicaClientFactory(AppConfigurationReplicaClientsBuilder clientBuilder, - AppConfigurationProperties properties) { - return new AppConfigurationReplicaClientFactory(clientBuilder, properties); - } - - /** - * Builder for clients connecting to App Configuration. - * - * @param properties Client configurations for setting up connections to each config store. - * @param appProperties Library configurations for setting up connections to each config store. - * @param tokenCredentialProviderOptional Optional provider for overriding Token Credentials for connecting to App - * Configuration. - * @param clientProviderOptional Optional client for overriding Client Connections to App Configuration stores. - * @param keyVaultCredentialProviderOptional optional provider, used to see if Key Vault is configured - * @param keyVaultClientProviderOptional optional client, used to see if Key Vault is configured - * @return ClientStore - */ - @Bean - @ConditionalOnMissingBean - AppConfigurationReplicaClientsBuilder replicaClientBuilder(AppConfigurationProperties properties, - AppConfigurationProviderProperties appProperties) { - - AppConfigurationReplicaClientsBuilder clientBuilder = new AppConfigurationReplicaClientsBuilder( - appProperties.getMaxRetries()); - - clientBuilder.setTokenCredentialProvider( - context.getBeanProvider(AppConfigurationCredentialProvider.class).getIfAvailable()); - clientBuilder - .setClientProvider(context.getBeanProvider(ConfigurationClientBuilderSetup.class).getIfAvailable()); - - KeyVaultCredentialProvider keyVaultCredentialProvider = context - .getBeanProvider(KeyVaultCredentialProvider.class).getIfAvailable(); - SecretClientBuilderSetup keyVaultClientProvider = context.getBeanProvider(SecretClientBuilderSetup.class) - .getIfAvailable(); - - if (keyVaultCredentialProvider != null || keyVaultClientProvider != null) { - clientBuilder.setKeyVaultConfigured(true); - } - - if (properties.getManagedIdentity() != null) { - clientBuilder.setClientId(properties.getManagedIdentity().getClientId()); - } - - return clientBuilder; - } -} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationCredentialProvider.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationCredentialProvider.java deleted file mode 100644 index 41f6b46de708f..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationCredentialProvider.java +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.config; - -import com.azure.core.credential.TokenCredential; - -/** - * Interface to be implemented that enables returning of a TokenCredential for authentication with an Azure App - * Configuration stores. - */ -public interface AppConfigurationCredentialProvider { - - /** - * Returns a Token Credential for connecting to the given endpoint. - * @param uri App Configuration endpoint - * @return TokenCredential for connecting to the uri. - */ - TokenCredential getAppConfigCredential(String uri); - -} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationHealthAutoConfiguration.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationHealthAutoConfiguration.java deleted file mode 100644 index a4c8a7d71edd6..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationHealthAutoConfiguration.java +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.config; - -import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; -import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import com.azure.spring.cloud.config.AppConfigurationAutoConfiguration.AppConfigurationWatchAutoConfiguration; -import com.azure.spring.cloud.config.health.AppConfigurationHealthIndicator; - -/** - * Health Indicator for Azure App Configuration store connections. - */ -@Configuration -@ConditionalOnClass({ HealthIndicator.class }) -@ConditionalOnEnabledHealthIndicator("azure-app-configuration") -@AutoConfigureAfter(AppConfigurationWatchAutoConfiguration.class) -public class AppConfigurationHealthAutoConfiguration { - - @Bean - @ConditionalOnBean(AppConfigurationRefresh.class) - AppConfigurationHealthIndicator appConfigurationHealthIndicator(AppConfigurationRefresh refresh) { - return new AppConfigurationHealthIndicator(refresh); - } - -} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/KeyVaultCredentialProvider.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/KeyVaultCredentialProvider.java deleted file mode 100644 index c790d130fc0b2..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/KeyVaultCredentialProvider.java +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.config; - -import com.azure.core.credential.TokenCredential; - -/** - * Interface to be implemented that enables returning of a TokenCredential for authentication with an Azure Key Vaults. - */ -public interface KeyVaultCredentialProvider { - - /** - * Returns a credential for a Key Vault given it's uri. - * @param uri URI to a key vault - * @return Token Credential - */ - TokenCredential getKeyVaultCredential(String uri); - -} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/TokenCredentialProvider.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/TokenCredentialProvider.java deleted file mode 100644 index c3cbd0cc2c87b..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/TokenCredentialProvider.java +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.config; - -import com.azure.core.credential.TokenCredential; - -/** - * Provides ability to generate Token Credential for connecting to Azure services. - */ -public interface TokenCredentialProvider { - - /** - * Returns a TokenCredential that will be used for connecting to Azure App Configuration. - * - * @param uri URI to App Configuration Store - * @return TokenCredential - */ - TokenCredential credentialForAppConfig(String uri); - - /** - * Returns a TokenCredential that will be used for connecting to Azure Key Vault. - * - * @param uri URI to Key Vault Instance - * @return TokenCredential - */ - TokenCredential credentialForKeyVault(String uri); - -} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/feature/management/entity/package-info.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/feature/management/entity/package-info.java deleted file mode 100644 index cc0dca2eaeeb6..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/feature/management/entity/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package contains classes for converting Azure App Configuration Feature Flags to the format used by the client library. - */ -package com.azure.spring.cloud.config.feature.management.entity; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/health/AppConfigurationHealthIndicator.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/health/AppConfigurationHealthIndicator.java deleted file mode 100644 index 666d841aa7525..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/health/AppConfigurationHealthIndicator.java +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.config.health; - -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.HealthIndicator; - -import com.azure.spring.cloud.config.AppConfigurationRefresh; - -/** - * Indicator class of App Configuration - */ -public final class AppConfigurationHealthIndicator implements HealthIndicator { - - private final AppConfigurationRefresh refresh; - - /** - * Indicator for the Health endpoint for connections to App Configurations. - * @param refresh App Configuration store refresher - */ - public AppConfigurationHealthIndicator(AppConfigurationRefresh refresh) { - this.refresh = refresh; - } - - @Override - public Health health() { - Health.Builder healthBuilder = new Health.Builder(); - boolean healthy = true; - - for (String store : refresh.getAppConfigurationStoresHealth().keySet()) { - if (AppConfigurationStoreHealth.DOWN.equals(refresh.getAppConfigurationStoresHealth().get(store))) { - healthy = false; - healthBuilder.withDetail(store, "DOWN"); - } else if (refresh.getAppConfigurationStoresHealth().get(store).equals(AppConfigurationStoreHealth.NOT_LOADED)) { - healthBuilder.withDetail(store, "NOT LOADED"); - } else { - healthBuilder.withDetail(store, "UP"); - } - } - - if (!healthy) { - return healthBuilder.down().build(); - } - return healthBuilder.up().build(); - } - -} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/health/package-info.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/health/package-info.java deleted file mode 100644 index fb97f5affe210..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/health/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package contains classes for converting Azure App Configuration Health information. - */ -package com.azure.spring.cloud.config.health; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySource.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySource.java deleted file mode 100644 index 39c2f4e0cb84d..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySource.java +++ /dev/null @@ -1,336 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.config.implementation; - -import static com.azure.spring.cloud.config.AppConfigurationConstants.FEATURE_FLAG_PREFIX; -import static com.azure.spring.cloud.config.AppConfigurationConstants.FEATURE_MANAGEMENT_KEY; -import static java.util.Collections.emptyList; -import static java.util.stream.Collectors.toMap; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.stream.IntStream; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.core.env.EnumerablePropertySource; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; - -import com.azure.data.appconfiguration.ConfigurationClient; -import com.azure.data.appconfiguration.models.ConfigurationSetting; -import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; -import com.azure.data.appconfiguration.models.FeatureFlagFilter; -import com.azure.data.appconfiguration.models.SecretReferenceConfigurationSetting; -import com.azure.data.appconfiguration.models.SettingSelector; -import com.azure.security.keyvault.secrets.models.KeyVaultSecret; -import com.azure.spring.cloud.config.KeyVaultCredentialProvider; -import com.azure.spring.cloud.config.KeyVaultSecretProvider; -import com.azure.spring.cloud.config.SecretClientBuilderSetup; -import com.azure.spring.cloud.config.feature.management.entity.Feature; -import com.azure.spring.cloud.config.feature.management.entity.FeatureSet; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationProviderProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreSelects; -import com.azure.spring.cloud.config.properties.ConfigStore; -import com.azure.spring.cloud.config.properties.FeatureFlagStore; -import com.azure.spring.cloud.config.stores.KeyVaultClient; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.json.JsonMapper; - -/** - * Azure App Configuration PropertySource unique per Store Label(Profile) combo. - * - *

- * i.e. If connecting to 2 stores and have 2 labels set 4 AppConfigurationPropertySources need to be created. - *

- */ -public final class AppConfigurationPropertySource extends EnumerablePropertySource { - - private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationPropertySource.class); - - private static final String USERS = "users"; - - private static final String USERS_CAPS = "Users"; - - private static final String AUDIENCE = "Audience"; - - private static final String GROUPS = "groups"; - - private static final String GROUPS_CAPS = "Groups"; - - private static final String TARGETING_FILTER = "targetingFilter"; - - private static final String DEFAULT_ROLLOUT_PERCENTAGE = "defaultRolloutPercentage"; - - private static final String DEFAULT_ROLLOUT_PERCENTAGE_CAPS = "DefaultRolloutPercentage"; - - private static final ObjectMapper CASE_INSENSITIVE_MAPPER = JsonMapper.builder() - .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build(); - - private static final ObjectMapper FEATURE_MAPPER = JsonMapper.builder() - .propertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE).build(); - - private final AppConfigurationStoreSelects selectedKeys; - - private final List profiles; - - private final Map properties = new LinkedHashMap<>(); - - private final AppConfigurationProperties appConfigurationProperties; - - private final Map keyVaultClients; - - private final AppConfigurationReplicaClient replicaClient; - - private final KeyVaultCredentialProvider keyVaultCredentialProvider; - - private final SecretClientBuilderSetup keyVaultClientProvider; - - private final KeyVaultSecretProvider keyVaultSecretProvider; - - private final AppConfigurationProviderProperties appProperties; - - private final FeatureFlagStore featureStore; - - AppConfigurationPropertySource(ConfigStore configStore, AppConfigurationStoreSelects selectedKeys, - List profiles, AppConfigurationProperties appConfigurationProperties, - AppConfigurationReplicaClient replicaClient, - AppConfigurationProviderProperties appProperties, KeyVaultCredentialProvider keyVaultCredentialProvider, - SecretClientBuilderSetup keyVaultClientProvider, KeyVaultSecretProvider keyVaultSecretProvider) { - // The context alone does not uniquely define a PropertySource, append storeName - // and label to uniquely define a PropertySource - super( - selectedKeys.getKeyFilter() + configStore.getEndpoint() + "/" + selectedKeys.getLabelFilterText(profiles)); - this.featureStore = configStore.getFeatureFlags(); - this.selectedKeys = selectedKeys; - this.profiles = profiles; - this.appConfigurationProperties = appConfigurationProperties; - this.appProperties = appProperties; - this.keyVaultClients = new HashMap<>(); - this.replicaClient = replicaClient; - this.keyVaultCredentialProvider = keyVaultCredentialProvider; - this.keyVaultClientProvider = keyVaultClientProvider; - this.keyVaultSecretProvider = keyVaultSecretProvider; - } - - private static List convertToListOrEmptyList(Map parameters, String key) { - List listObjects = CASE_INSENSITIVE_MAPPER.convertValue(parameters.get(key), - new TypeReference>() { - }); - return listObjects == null ? emptyList() : listObjects; - } - - @Override - public String[] getPropertyNames() { - Set keySet = properties.keySet(); - return keySet.toArray(new String[keySet.size()]); - } - - @Override - public Object getProperty(String name) { - return properties.get(name); - } - - /** - *

- * Gets settings from Azure/Cache to set as configurations. Updates the cache. - *

- * - *

- * Note: Doesn't update Feature Management, just stores values in cache. Call {@code initFeatures} to update - * Feature Management, but make sure its done in the last {@code AppConfigurationPropertySource} - * AppConfigurationPropertySource} - *

- * - * @param featureSet The set of Feature Management Flags from various config stores. - * @return Updated Feature Set from Property Source - * @throws IOException Thrown when processing key/value failed when reading feature flags - * @throws AppConfigurationStatusException An error occurred in connecting, and should be retried on a replica if - * possible. - */ - FeatureSet initProperties(FeatureSet featureSet) throws IOException, AppConfigurationStatusException { - SettingSelector settingSelector = new SettingSelector(); - - List features = null; - // Reading In Features - if (featureStore.getEnabled()) { - settingSelector.setKeyFilter(featureStore.getKeyFilter()).setLabelFilter(featureStore.getLabelFilter()); - - features = replicaClient.listConfigurationSettings(settingSelector); - } - - List labels = Arrays.asList(selectedKeys.getLabelFilter(profiles)); - Collections.reverse(labels); - - for (String label : labels) { - settingSelector = new SettingSelector().setKeyFilter(selectedKeys.getKeyFilter() + "*") - .setLabelFilter(label); - - // * for wildcard match - List settings = replicaClient.listConfigurationSettings(settingSelector); - for (ConfigurationSetting setting : settings) { - String key = setting.getKey().trim().substring(selectedKeys.getKeyFilter().length()).replace('/', '.'); - if (setting instanceof SecretReferenceConfigurationSetting) { - String entry = getKeyVaultEntry((SecretReferenceConfigurationSetting) setting); - - // Null in the case of failFast is false, will just skip entry. - if (entry != null) { - properties.put(key, entry); - } - } else if (StringUtils.hasText(setting.getContentType()) - && JsonConfigurationParser.isJsonContentType(setting.getContentType())) { - Map jsonSettings = JsonConfigurationParser.parseJsonSetting(setting); - for (Entry jsonSetting : jsonSettings.entrySet()) { - key = jsonSetting.getKey().trim().substring(selectedKeys.getKeyFilter().length()); - properties.put(key, jsonSetting.getValue()); - } - } else { - properties.put(key, setting.getValue()); - } - } - } - return addToFeatureSet(featureSet, features); - } - - /** - * Given a Setting's Key Vault Reference stored in the Settings value, it will get its entry in Key Vault. - * - * @param secretReference {"uri": "<your-vault-url>/secret/<secret>/<version>"} - * @return Key Vault Secret Value - */ - private String getKeyVaultEntry(SecretReferenceConfigurationSetting secretReference) { - String secretValue = null; - try { - URI uri = null; - - // Parsing Key Vault Reference for URI - try { - uri = new URI(secretReference.getSecretId()); - } catch (URISyntaxException e) { - LOGGER.error("Error Processing Key Vault Entry URI."); - ReflectionUtils.rethrowRuntimeException(e); - } - - // Check if we already have a client for this key vault, if not we will make - // one - if (!keyVaultClients.containsKey(uri.getHost())) { - KeyVaultClient client = new KeyVaultClient(appConfigurationProperties, uri, keyVaultCredentialProvider, - keyVaultClientProvider, keyVaultSecretProvider); - keyVaultClients.put(uri.getHost(), client); - } - KeyVaultSecret secret = keyVaultClients.get(uri.getHost()).getSecret(uri, appProperties.getMaxRetryTime()); - if (secret == null) { - throw new IOException("No Key Vault Secret found for Reference."); - } - secretValue = secret.getValue(); - } catch (RuntimeException | IOException e) { - LOGGER.error("Error Retrieving Key Vault Entry"); - ReflectionUtils.rethrowRuntimeException(e); - } - return secretValue; - } - - /** - * Initializes Feature Management configurations. Only one {@code AppConfigurationPropertySource} can call this, and - * it needs to be done after the rest have run initProperties. - * - * @param featureSet Feature Flag info to be set to this property source. - */ - void initFeatures(FeatureSet featureSet) { - properties.put(FEATURE_MANAGEMENT_KEY, - FEATURE_MAPPER.convertValue(featureSet.getFeatureManagement(), LinkedHashMap.class)); - } - - private FeatureSet addToFeatureSet(FeatureSet featureSet, List features) - throws IOException { - if (features == null) { - return featureSet; - } - // Reading In Features - for (ConfigurationSetting setting : features) { - if (setting instanceof FeatureFlagConfigurationSetting) { - Object feature = createFeature((FeatureFlagConfigurationSetting) setting); - featureSet.addFeature(setting.getKey().trim().substring(FEATURE_FLAG_PREFIX.length()), feature); - } - } - return featureSet; - } - - /** - * Creates a {@code Feature} from a {@code KeyValueItem} - * - * @param item Used to create Features before being converted to be set into properties. - * @return Feature created from KeyValueItem - */ - @SuppressWarnings("unchecked") - private Object createFeature(FeatureFlagConfigurationSetting item) { - String key = getFeatureSimpleName(item); - Feature feature = new Feature(key, item); - Map featureEnabledFor = feature.getEnabledFor(); - - // Setting Enabled For to null, but enabled = true will result in the feature - // being on. This is the case of a feature is on/off and set to on. This is to - // tell the difference between conditional/off which looks exactly the same... - // It should never be the case of Conditional On, and no filters coming from - // Azure, but it is a valid way from the config file, which should result in - // false being returned. - if (featureEnabledFor.size() == 0 && item.isEnabled()) { - return true; - } else if (!item.isEnabled()) { - return false; - } - for (int filter = 0; filter < feature.getEnabledFor().size(); filter++) { - FeatureFlagFilter featureFilterEvaluationContext = featureEnabledFor.get(filter); - Map parameters = featureFilterEvaluationContext.getParameters(); - - if (parameters == null || !TARGETING_FILTER.equals(featureEnabledFor.get(filter).getName())) { - continue; - } - - Object audienceObject = parameters.get(AUDIENCE); - if (audienceObject != null) { - parameters = (Map) audienceObject; - } - - List users = convertToListOrEmptyList(parameters, USERS_CAPS); - List groupRollouts = convertToListOrEmptyList(parameters, GROUPS_CAPS); - - switchKeyValues(parameters, USERS_CAPS, USERS, mapValuesByIndex(users)); - switchKeyValues(parameters, GROUPS_CAPS, GROUPS, mapValuesByIndex(groupRollouts)); - switchKeyValues(parameters, DEFAULT_ROLLOUT_PERCENTAGE_CAPS, DEFAULT_ROLLOUT_PERCENTAGE, - parameters.get(DEFAULT_ROLLOUT_PERCENTAGE_CAPS)); - - featureFilterEvaluationContext.setParameters(parameters); - featureEnabledFor.put(filter, featureFilterEvaluationContext); - feature.setEnabledFor(featureEnabledFor); - } - return feature; - - } - - private String getFeatureSimpleName(ConfigurationSetting setting) { - return setting.getKey().trim().substring(FEATURE_FLAG_PREFIX.length()); - } - - private Map mapValuesByIndex(List users) { - return IntStream.range(0, users.size()).boxed().collect(toMap(String::valueOf, users::get)); - } - - private void switchKeyValues(Map parameters, String oldKey, String newKey, Object value) { - parameters.put(newKey, value); - parameters.remove(oldKey); - } -} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java deleted file mode 100644 index 2eddf839c03d7..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.config.implementation; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.EnvironmentAware; -import org.springframework.core.env.Environment; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -import com.azure.core.credential.TokenCredential; -import com.azure.core.http.policy.ExponentialBackoff; -import com.azure.core.http.policy.RetryPolicy; -import com.azure.data.appconfiguration.ConfigurationClientBuilder; -import com.azure.identity.ManagedIdentityCredentialBuilder; -import com.azure.spring.cloud.config.AppConfigurationCredentialProvider; -import com.azure.spring.cloud.config.ConfigurationClientBuilderSetup; -import com.azure.spring.cloud.config.pipline.policies.BaseAppConfigurationPolicy; -import com.azure.spring.cloud.config.properties.ConfigStore; - -public class AppConfigurationReplicaClientsBuilder implements EnvironmentAware { - - private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationReplicaClientsBuilder.class); - - /** - * Invalid Connection String error message - */ - public static final String NON_EMPTY_MSG = "%s property should not be null or empty in the connection string of Azure Config Service."; - - private static final Duration DEFAULT_MIN_RETRY_POLICY = Duration.ofMillis(800); - - private static final Duration DEFAULT_MAX_RETRY_POLICY = Duration.ofSeconds(8); - - /** - * Connection String Regex format - */ - private static final String CONN_STRING_REGEXP = "Endpoint=([^;]+);Id=([^;]+);Secret=([^;]+)"; - - /** - * Invalid Formatted Connection String Error message - */ - public static final String ENDPOINT_ERR_MSG = String.format("Connection string does not follow format %s.", - CONN_STRING_REGEXP); - - private static final Pattern CONN_STRING_PATTERN = Pattern.compile(CONN_STRING_REGEXP); - - private AppConfigurationCredentialProvider tokenCredentialProvider; - - private ConfigurationClientBuilderSetup clientProvider; - - private boolean isDev = false; - - private boolean isKeyVaultConfigured = false; - - private String clientId = ""; - - private final int maxRetries; - - public AppConfigurationReplicaClientsBuilder(int maxRetries) { - this.maxRetries = maxRetries; - } - - /** - * @param tokenCredentialProvider the tokenCredentialProvider to set - */ - public void setTokenCredentialProvider(AppConfigurationCredentialProvider tokenCredentialProvider) { - this.tokenCredentialProvider = tokenCredentialProvider; - } - - /** - * @param clientProvider the clientProvider to set - */ - public void setClientProvider(ConfigurationClientBuilderSetup clientProvider) { - this.clientProvider = clientProvider; - } - - /** - * @param isKeyVaultConfigured the isKeyVaultConfigured to set - */ - public void setKeyVaultConfigured(boolean isKeyVaultConfigured) { - this.isKeyVaultConfigured = isKeyVaultConfigured; - } - - /** - * @param clientId the clientId to set - */ - public void setClientId(String clientId) { - this.clientId = clientId; - } - - /** - * Given a connection string, returns the endpoint value inside of it. - * @param connectionString connection string to app configuration - * @return endpoint - * @throws IllegalStateException when connection string isn't valid. - */ - public static String getEndpointFromConnectionString(String connectionString) { - Assert.hasText(connectionString, "Connection string cannot be empty."); - - Matcher matcher = CONN_STRING_PATTERN.matcher(connectionString); - if (!matcher.find()) { - throw new IllegalStateException(ENDPOINT_ERR_MSG); - } - - String endpoint = matcher.group(1); - - Assert.hasText(endpoint, String.format(NON_EMPTY_MSG, "Endpoint")); - - return endpoint; - } - - /** - * Builds all the clients for a connection. - * @throws IllegalArgumentException when more than 1 connection method is given. - */ - List buildClients(ConfigStore configStore) { - List clients = new ArrayList<>(); - // Single client or Multiple? - // If single call buildClient - int hasSingleConnectionString = StringUtils.hasText(configStore.getConnectionString()) ? 1 : 0; - int hasMultiEndpoints = configStore.getEndpoints().size() > 0 ? 1 : 0; - int hasMultiConnectionString = configStore.getConnectionStrings().size() > 0 ? 1 : 0; - - if (hasSingleConnectionString + hasMultiEndpoints + hasMultiConnectionString > 1) { - throw new IllegalArgumentException( - "More than 1 Connection method was set for connecting to App Configuration."); - } - - TokenCredential tokenCredential = null; - - if (tokenCredentialProvider != null) { - tokenCredential = tokenCredentialProvider.getAppConfigCredential(configStore.getEndpoint()); - } - - boolean clientIdIsPresent = StringUtils.hasText(clientId); - boolean tokenCredentialIsPresent = tokenCredential != null; - boolean connectionStringIsPresent = configStore.getConnectionString() != null; - - if ((tokenCredentialIsPresent || clientIdIsPresent) - && connectionStringIsPresent) { - throw new IllegalArgumentException( - "More than 1 Connection method was set for connecting to App Configuration."); - } else if (tokenCredential != null && clientIdIsPresent) { - throw new IllegalArgumentException( - "More than 1 Connection method was set for connecting to App Configuration."); - } - - ConfigurationClientBuilder builder = getBuilder(); - - if (configStore.getConnectionString() != null) { - clients.add(buildClientConnectionString(configStore.getConnectionString(), builder, 0)); - } else if (configStore.getConnectionStrings().size() > 0) { - for (String connectionString : configStore.getConnectionStrings()) { - clients.add(buildClientConnectionString(connectionString, builder, - configStore.getConnectionStrings().size() - 1)); - } - } else if (configStore.getEndpoints().size() > 0) { - for (String endpoint : configStore.getEndpoints()) { - clients.add(buildClientEndpoint(tokenCredential, endpoint, builder, clientIdIsPresent, - configStore.getEndpoints().size() - 1)); - } - } else if (configStore.getEndpoint() != null) { - clients.add(buildClientEndpoint(tokenCredential, configStore.getEndpoint(), builder, clientIdIsPresent, 0)); - } - return clients; - } - - /** - * @return creates an instance of ConfigurationClientBuilder - */ - ConfigurationClientBuilder getBuilder() { - return new ConfigurationClientBuilder(); - } - - private AppConfigurationReplicaClient buildClientEndpoint(TokenCredential tokenCredential, - String endpoint, ConfigurationClientBuilder builder, boolean clientIdIsPresent, Integer replicaCount) - throws IllegalArgumentException { - if (tokenCredential != null) { - // User Provided Token Credential - LOGGER.debug("Connecting to " + endpoint + " using AppConfigurationCredentialProvider."); - builder.credential(tokenCredential); - } else if (clientIdIsPresent) { - // User Assigned Identity - Client ID through configuration file. - LOGGER.debug("Connecting to " + endpoint + " using Client ID from configuration file."); - ManagedIdentityCredentialBuilder micBuilder = new ManagedIdentityCredentialBuilder() - .clientId(clientId); - builder.credential(micBuilder.build()); - } else { - // System Assigned Identity. Needs to be checked last as all of the above should have an Endpoint. - LOGGER.debug("Connecting to " + endpoint - + " using Azure System Assigned Identity or Azure User Assigned Identity."); - ManagedIdentityCredentialBuilder micBuilder = new ManagedIdentityCredentialBuilder(); - builder.credential(micBuilder.build()); - } - - builder.endpoint(endpoint); - - return modifyAndBuildClient(builder, endpoint, replicaCount); - } - - private AppConfigurationReplicaClient buildClientConnectionString(String connectionString, - ConfigurationClientBuilder builder, Integer replicaCount) - throws IllegalArgumentException { - String endpoint = getEndpointFromConnectionString(connectionString); - LOGGER.debug("Connecting to " + endpoint + " using Connecting String."); - - builder.connectionString(connectionString); - - return modifyAndBuildClient(builder, endpoint, replicaCount); - } - - private AppConfigurationReplicaClient modifyAndBuildClient(ConfigurationClientBuilder builder, String endpoint, - Integer replicaCount) { - ExponentialBackoff retryPolicy = new ExponentialBackoff(maxRetries, DEFAULT_MIN_RETRY_POLICY, - DEFAULT_MAX_RETRY_POLICY); - - builder.addPolicy(new BaseAppConfigurationPolicy(isDev, isKeyVaultConfigured, replicaCount)) - .retryPolicy(new RetryPolicy(retryPolicy)); - - if (clientProvider != null) { - clientProvider.setup(builder, endpoint); - } - - return new AppConfigurationReplicaClient(endpoint, builder.buildClient()); - } - - @Override - public void setEnvironment(Environment environment) { - for (String profile : environment.getActiveProfiles()) { - if ("dev".equalsIgnoreCase(profile)) { - this.isDev = true; - break; - } - } - } -} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/pipline/policies/package-info.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/pipline/policies/package-info.java deleted file mode 100644 index abdfc64fa2428..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/pipline/policies/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package contains classes for properties used to configure the library. - */ -package com.azure.spring.cloud.config.pipline.policies; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/FeatureFlagStore.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/FeatureFlagStore.java deleted file mode 100644 index e3c8556470ae6..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/FeatureFlagStore.java +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.config.properties; - -import static com.azure.spring.cloud.config.AppConfigurationConstants.EMPTY_LABEL; -import static com.azure.spring.cloud.config.AppConfigurationConstants.FEATURE_STORE_WATCH_KEY; - -/** - * Properties for what needs to be requested from Azure App Configuration for Feature Flags. - */ -public final class FeatureFlagStore { - - /** - * Boolean for if feature flag loading is enabled. - */ - private Boolean enabled = false; - - /** - * App Configuration \0 empty label, when no label is set. - */ - private String labelFilter = EMPTY_LABEL; - - /** - * @return the enabled - */ - public Boolean getEnabled() { - return enabled; - } - - /** - * @param enabled the enabled to set - */ - public void setEnabled(Boolean enabled) { - this.enabled = enabled; - } - - /** - * @return the keyFilter - */ - public String getKeyFilter() { - return FEATURE_STORE_WATCH_KEY; - } - - /** - * @return the labelFilter - */ - public String getLabelFilter() { - return labelFilter; - } - - /** - * @param labelFilter the labelFilter to set - */ - public void setLabelFilter(String labelFilter) { - this.labelFilter = labelFilter; - } -} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/package-info.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/package-info.java deleted file mode 100644 index 49544bf4daed2..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package contains classes for properties to configure the library. - */ -package com.azure.spring.cloud.config.properties; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/resource/AppConfigManagedIdentityProperties.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/resource/AppConfigManagedIdentityProperties.java deleted file mode 100644 index e7871946eb167..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/resource/AppConfigManagedIdentityProperties.java +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.config.resource; - -import org.springframework.lang.Nullable; - -/** - * Managed Identity information for connecting to Azure App Configuration Stores. - */ -public final class AppConfigManagedIdentityProperties { - - @Nullable - private String clientId; // Optional: client_id of the managed identity - - /** - * @return the clientId - */ - @Nullable - public String getClientId() { - return clientId; - } - - /** - * @param clientId the clientId to set - */ - public void setClientId(@Nullable String clientId) { - this.clientId = clientId; - } - - -} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/resource/package-info.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/resource/package-info.java deleted file mode 100644 index e23da31b01f38..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/resource/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package contains classes for connecting to Azure App Configuration Stores. - */ -package com.azure.spring.cloud.config.resource; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/stores/KeyVaultClient.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/stores/KeyVaultClient.java deleted file mode 100644 index d2e09832a5aa5..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/stores/KeyVaultClient.java +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.config.stores; - -import java.net.URI; -import java.time.Duration; - -import org.springframework.util.StringUtils; - -import com.azure.core.credential.TokenCredential; -import com.azure.identity.ManagedIdentityCredentialBuilder; -import com.azure.security.keyvault.secrets.SecretAsyncClient; -import com.azure.security.keyvault.secrets.SecretClientBuilder; -import com.azure.security.keyvault.secrets.models.KeyVaultSecret; -import com.azure.spring.cloud.config.KeyVaultCredentialProvider; -import com.azure.spring.cloud.config.KeyVaultSecretProvider; -import com.azure.spring.cloud.config.SecretClientBuilderSetup; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.resource.AppConfigManagedIdentityProperties; - -/** - * Client for connecting to and getting secrets from a Key Vault - */ -public final class KeyVaultClient { - - private SecretAsyncClient secretClient; - - private final AppConfigurationProperties properties; - - private final SecretClientBuilderSetup keyVaultClientProvider; - - private final URI uri; - - private final TokenCredential tokenCredential; - - private final KeyVaultSecretProvider keyVaultSecretProvider; - - private Boolean useSecretResolver = false; - - /** - * Creates a Client for connecting to Key Vault - * @param properties AppConfiguration Properties - * @param uri Key Vault URI - * @param tokenCredentialProvider optional provider of the Token Credential for connecting to Key Vault - * @param keyVaultClientProvider optional provider for overriding the Key Vault Client - * @param keyVaultSecretProvider optional provider for providing Secrets instead of connecting to Key Vault - */ - public KeyVaultClient(AppConfigurationProperties properties, URI uri, - KeyVaultCredentialProvider tokenCredentialProvider, SecretClientBuilderSetup keyVaultClientProvider, - KeyVaultSecretProvider keyVaultSecretProvider) { - this.properties = properties; - this.uri = uri; - if (tokenCredentialProvider != null) { - this.tokenCredential = tokenCredentialProvider.getKeyVaultCredential("https://" + uri.getHost()); - } else { - this.tokenCredential = null; - } - this.keyVaultClientProvider = keyVaultClientProvider; - this.keyVaultSecretProvider = keyVaultSecretProvider; - } - - KeyVaultClient build() { - SecretClientBuilder builder = getBuilder(); - AppConfigManagedIdentityProperties msiProps = properties.getManagedIdentity(); - String fullUri = "https://" + uri.getHost(); - - if (tokenCredential != null && msiProps != null) { - throw new IllegalArgumentException("More than 1 Connection method was set for connecting to Key Vault."); - } - - if (tokenCredential != null) { - // User Provided Token Credential - builder.credential(tokenCredential); - } else if (msiProps != null && StringUtils.hasText(msiProps.getClientId())) { - // User Assigned Identity - Client ID through configuration file. - builder.credential(new ManagedIdentityCredentialBuilder().clientId(msiProps.getClientId()).build()); - } else if (keyVaultSecretProvider != null) { // This is the Secret Resolver - // Use this instead. - useSecretResolver = true; - } else { - // System Assigned Identity. - builder.credential(new ManagedIdentityCredentialBuilder().build()); - } - builder.vaultUrl(fullUri); - - if (keyVaultClientProvider != null) { - keyVaultClientProvider.setup(builder, fullUri); - } - - if (!useSecretResolver) { - secretClient = builder.buildAsyncClient(); - } - - return this; - } - - /** - * Gets the specified secret using the Secret Identifier - * - * @param secretIdentifier The Secret Identifier to Secret - * @param timeout How long it waits for a response from Key Vault - * @return Secret values that matches the secretIdentifier - */ - public KeyVaultSecret getSecret(URI secretIdentifier, int timeout) { - if (secretClient == null && !useSecretResolver) { - build(); - } - - if (useSecretResolver) { // Secret Resolver - return new KeyVaultSecret(null, keyVaultSecretProvider.getSecret(secretIdentifier.getRawPath())); - } - - String[] tokens = secretIdentifier.getPath().split("/"); - - String name = (tokens.length >= 3 ? tokens[2] : null); - String version = (tokens.length >= 4 ? tokens[3] : null); - return secretClient.getSecret(name, version).block(Duration.ofSeconds(timeout)); - } - - SecretClientBuilder getBuilder() { - return new SecretClientBuilder(); - } - -} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/stores/package-info.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/stores/package-info.java deleted file mode 100644 index 046fcc1c3a6b1..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/stores/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package contains classes for using the Azure SDK for Java. - */ -package com.azure.spring.cloud.config.stores; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/resources/META-INF/spring.factories b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 1c941108d5f11..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,6 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -com.azure.spring.cloud.config.AppConfigurationAutoConfiguration, com.azure.spring.cloud.config.AppConfigurationHealthAutoConfiguration - -org.springframework.cloud.bootstrap.BootstrapConfiguration=\ -com.azure.spring.cloud.config.AppConfigurationBootstrapConfiguration - diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationHealthIndicatorTest.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationHealthIndicatorTest.java deleted file mode 100644 index 9bfcf4e19862f..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationHealthIndicatorTest.java +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.config.implementation; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Status; - -import com.azure.spring.cloud.config.AppConfigurationRefresh; -import com.azure.spring.cloud.config.health.AppConfigurationHealthIndicator; -import com.azure.spring.cloud.config.health.AppConfigurationStoreHealth; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.properties.ConfigStore; - -public class AppConfigurationHealthIndicatorTest { - - @Mock - private AppConfigurationRefresh refreshMock; - - @BeforeEach - public void setup() { - MockitoAnnotations.openMocks(this); - } - - @Test - public void noConfigurationStores() { - AppConfigurationHealthIndicator indicator = new AppConfigurationHealthIndicator(refreshMock); - Map storeHealth = new HashMap<>(); - - when(refreshMock.getAppConfigurationStoresHealth()).thenReturn(storeHealth); - - Health health = indicator.health(); - assertEquals(Status.UP, health.getStatus()); - assertEquals(0, health.getDetails().size()); - } - - @Test - public void healthyConfigurationStore() { - String storeName = "singleHealthyStoreIndicatorTest"; - - AppConfigurationHealthIndicator indicator = new AppConfigurationHealthIndicator(refreshMock); - Map storeHealth = new HashMap<>(); - - storeHealth.put(storeName, AppConfigurationStoreHealth.UP); - - when(refreshMock.getAppConfigurationStoresHealth()).thenReturn(storeHealth); - - Health health = indicator.health(); - assertEquals(Status.UP, health.getStatus()); - assertEquals(1, health.getDetails().size()); - assertEquals("UP", health.getDetails().get(storeName)); - } - - @Test - public void unloadedConfigurationStore() { - String storeName = "singleUnloadedStoreIndicatorTest"; - - AppConfigurationProperties properties = new AppConfigurationProperties(); - List stores = new ArrayList<>(); - - ConfigStore store = new ConfigStore(); - store.setEndpoint(storeName); - store.setEnabled(true); - stores.add(store); - - properties.setStores(stores); - - AppConfigurationHealthIndicator indicator = new AppConfigurationHealthIndicator(refreshMock); - - Map mockHealth = new HashMap<>(); - - mockHealth.put(storeName, AppConfigurationStoreHealth.NOT_LOADED); - - when(refreshMock.getAppConfigurationStoresHealth()).thenReturn(mockHealth); - - Health health = indicator.health(); - assertEquals(Status.UP, health.getStatus()); - assertEquals(1, health.getDetails().size()); - assertEquals("NOT LOADED", health.getDetails().get(storeName)); - } - - @Test - public void unhealthyConfigurationStore() { - String storeName = "singleUnhealthyStoreIndicatorTest"; - - AppConfigurationHealthIndicator indicator = new AppConfigurationHealthIndicator(refreshMock); - - Map healthStatus = new HashMap<>(); - - healthStatus.put(storeName, AppConfigurationStoreHealth.DOWN); - - when(refreshMock.getAppConfigurationStoresHealth()).thenReturn(healthStatus); - - Health health = indicator.health(); - assertEquals(Status.DOWN, health.getStatus()); - assertEquals(1, health.getDetails().size()); - assertEquals("DOWN", health.getDetails().get(storeName)); - } - -} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceTest.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceTest.java deleted file mode 100644 index 3cc98b343b1df..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceTest.java +++ /dev/null @@ -1,466 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.config.implementation; - -import static com.azure.spring.cloud.config.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE; -import static com.azure.spring.cloud.config.TestConstants.FEATURE_BOOLEAN_VALUE; -import static com.azure.spring.cloud.config.TestConstants.FEATURE_LABEL; -import static com.azure.spring.cloud.config.TestConstants.FEATURE_VALUE; -import static com.azure.spring.cloud.config.TestConstants.FEATURE_VALUE_PARAMETERS; -import static com.azure.spring.cloud.config.TestConstants.FEATURE_VALUE_TARGETING; -import static com.azure.spring.cloud.config.TestConstants.TEST_CONN_STRING; -import static com.azure.spring.cloud.config.TestConstants.TEST_KEY_1; -import static com.azure.spring.cloud.config.TestConstants.TEST_KEY_2; -import static com.azure.spring.cloud.config.TestConstants.TEST_KEY_3; -import static com.azure.spring.cloud.config.TestConstants.TEST_LABEL_1; -import static com.azure.spring.cloud.config.TestConstants.TEST_LABEL_2; -import static com.azure.spring.cloud.config.TestConstants.TEST_LABEL_3; -import static com.azure.spring.cloud.config.TestConstants.TEST_SLASH_KEY; -import static com.azure.spring.cloud.config.TestConstants.TEST_SLASH_VALUE; -import static com.azure.spring.cloud.config.TestConstants.TEST_STORE_NAME; -import static com.azure.spring.cloud.config.TestConstants.TEST_VALUE_1; -import static com.azure.spring.cloud.config.TestConstants.TEST_VALUE_2; -import static com.azure.spring.cloud.config.TestConstants.TEST_VALUE_3; -import static com.azure.spring.cloud.config.implementation.TestUtils.createItem; -import static com.azure.spring.cloud.config.implementation.TestUtils.createItemFeatureFlag; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.fail; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import com.azure.core.http.rest.PagedFlux; -import com.azure.core.http.rest.PagedResponse; -import com.azure.data.appconfiguration.ConfigurationAsyncClient; -import com.azure.data.appconfiguration.models.ConfigurationSetting; -import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; -import com.azure.data.appconfiguration.models.FeatureFlagFilter; -import com.azure.spring.cloud.config.KeyVaultCredentialProvider; -import com.azure.spring.cloud.config.feature.management.entity.Feature; -import com.azure.spring.cloud.config.feature.management.entity.FeatureSet; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationProviderProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreSelects; -import com.azure.spring.cloud.config.properties.ConfigStore; -import com.azure.spring.cloud.config.properties.FeatureFlagStore; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -public class AppConfigurationPropertySourceTest { - - public static final List FEATURE_ITEMS = new ArrayList<>(); - - public static final List FEATURE_ITEMS_TARGETING = new ArrayList<>(); - - private static final String EMPTY_CONTENT_TYPE = ""; - - private static final String USERS = "users"; - - private static final String GROUPS = "groups"; - - private static final String DEFAULT_ROLLOUT_PERCENTAGE = "defaultRolloutPercentage"; - - private static final AppConfigurationProperties TEST_PROPS = new AppConfigurationProperties(); - - private static final String KEY_FILTER = "/foo/"; - - private static final ConfigurationSetting ITEM_1 = createItem(KEY_FILTER, TEST_KEY_1, TEST_VALUE_1, TEST_LABEL_1, - EMPTY_CONTENT_TYPE); - - private static final ConfigurationSetting ITEM_2 = createItem(KEY_FILTER, TEST_KEY_2, TEST_VALUE_2, TEST_LABEL_2, - EMPTY_CONTENT_TYPE); - - private static final ConfigurationSetting ITEM_3 = createItem(KEY_FILTER, TEST_KEY_3, TEST_VALUE_3, TEST_LABEL_3, - EMPTY_CONTENT_TYPE); - - private static final ConfigurationSetting ITEM_NULL = createItem(KEY_FILTER, TEST_KEY_3, TEST_VALUE_3, TEST_LABEL_3, - null); - - private static final FeatureFlagConfigurationSetting FEATURE_ITEM = createItemFeatureFlag(".appconfig.featureflag/", - "Alpha", - FEATURE_VALUE, FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE); - - private static final FeatureFlagConfigurationSetting FEATURE_ITEM_2 = createItemFeatureFlag( - ".appconfig.featureflag/", "Beta", - FEATURE_BOOLEAN_VALUE, FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE); - - private static final FeatureFlagConfigurationSetting FEATURE_ITEM_3 = createItemFeatureFlag( - ".appconfig.featureflag/", "Gamma", - FEATURE_VALUE_PARAMETERS, FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE); - - private static final FeatureFlagConfigurationSetting FEATURE_ITEM_NULL = createItemFeatureFlag( - ".appconfig.featureflag/", "Alpha", - FEATURE_VALUE, - FEATURE_LABEL, null); - - private static final FeatureFlagConfigurationSetting FEATURE_ITEM_TARGETING = createItemFeatureFlag( - ".appconfig.featureflag/", "target", - FEATURE_VALUE_TARGETING, FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE); - - private static final String FEATURE_MANAGEMENT_KEY = "feature-management.featureManagement"; - - private static ObjectMapper mapper = new ObjectMapper(); - - private List testItems = new ArrayList<>(); - - private AppConfigurationPropertySource propertySource; - - private AppConfigurationProperties appConfigurationProperties; - - @Mock - private AppConfigurationReplicaClient clientMock; - - @Mock - private ConfigurationAsyncClient configClientMock; - - @Mock - private PagedFlux settingsMock; - - @Mock - private Flux> pageMock; - - @Mock - private Mono>> collectionMock; - - @Mock - private List> itemsMock; - - @Mock - private Iterator> itemsIteratorMock; - - @Mock - private PagedResponse pagedResponseMock; - - @Mock - private ConfigStore configStoreMock; - - private FeatureFlagStore featureFlagStore; - - private AppConfigurationProviderProperties appProperties; - - private KeyVaultCredentialProvider tokenCredentialProvider = null; - - @Mock - private List configurationListMock; - - @BeforeAll - public static void setup() { - TestUtils.addStore(TEST_PROPS, TEST_STORE_NAME, TEST_CONN_STRING, KEY_FILTER); - - FEATURE_ITEM.setContentType(FEATURE_FLAG_CONTENT_TYPE); - FEATURE_ITEMS.add(FEATURE_ITEM); - FEATURE_ITEMS.add(FEATURE_ITEM_2); - FEATURE_ITEMS.add(FEATURE_ITEM_3); - - FEATURE_ITEMS_TARGETING.add(FEATURE_ITEM_TARGETING); - } - - @BeforeEach - public void init() { - mapper.setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE); - - MockitoAnnotations.openMocks(this); - appConfigurationProperties = new AppConfigurationProperties(); - appProperties = new AppConfigurationProviderProperties(); - - AppConfigurationStoreSelects selectedKeys = new AppConfigurationStoreSelects().setKeyFilter(KEY_FILTER) - .setLabelFilter("\0"); - - testItems = new ArrayList<>(); - testItems.add(ITEM_1); - testItems.add(ITEM_2); - testItems.add(ITEM_3); - - when(configStoreMock.getEndpoint()).thenReturn(TEST_STORE_NAME); - featureFlagStore = new FeatureFlagStore(); - when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStore); - when(configClientMock.listConfigurationSettings(Mockito.any())).thenReturn(settingsMock); - when(settingsMock.byPage()).thenReturn(pageMock); - when(pageMock.collectList()).thenReturn(collectionMock); - when(collectionMock.block()).thenReturn(itemsMock); - when(itemsMock.iterator()).thenReturn(itemsIteratorMock); - when(itemsIteratorMock.next()).thenReturn(pagedResponseMock); - - propertySource = new AppConfigurationPropertySource(configStoreMock, selectedKeys, new ArrayList<>(), - appConfigurationProperties, clientMock, appProperties, tokenCredentialProvider, null, null); - } - - @AfterEach - public void cleanup() throws Exception { - MockitoAnnotations.openMocks(this).close(); - } - - @Test - public void testPropCanBeInitAndQueried() throws AppConfigurationStatusException, IOException { - when(configurationListMock.iterator()).thenReturn(testItems.iterator()).thenReturn(FEATURE_ITEMS.iterator()); - when(clientMock.listConfigurationSettings(Mockito.any())).thenReturn(configurationListMock) - .thenReturn(configurationListMock); - - FeatureSet featureSet = new FeatureSet(); - propertySource.initProperties(featureSet); - propertySource.initFeatures(featureSet); - - String[] keyNames = propertySource.getPropertyNames(); - String[] expectedKeyNames = testItems.stream() - .map(t -> t.getKey().substring(KEY_FILTER.length())).toArray(String[]::new); - String[] allExpectedKeyNames = new String[expectedKeyNames.length + 1]; - - String[] featureManagementKey = { FEATURE_MANAGEMENT_KEY }; - - System.arraycopy(expectedKeyNames, 0, allExpectedKeyNames, 0, expectedKeyNames.length); - System.arraycopy(featureManagementKey, 0, allExpectedKeyNames, expectedKeyNames.length, 1); - - assertThat(keyNames).containsExactlyInAnyOrder(allExpectedKeyNames); - - assertThat(propertySource.getProperty(TEST_KEY_1)).isEqualTo(TEST_VALUE_1); - assertThat(propertySource.getProperty(TEST_KEY_2)).isEqualTo(TEST_VALUE_2); - assertThat(propertySource.getProperty(TEST_KEY_3)).isEqualTo(TEST_VALUE_3); - } - - @Test - public void testPropertyNameSlashConvertedToDots() throws AppConfigurationStatusException, IOException { - ConfigurationSetting slashedProp = createItem(KEY_FILTER, TEST_SLASH_KEY, TEST_SLASH_VALUE, null, - EMPTY_CONTENT_TYPE); - List settings = new ArrayList<>(); - settings.add(slashedProp); - when(configurationListMock.iterator()).thenReturn(settings.iterator()) - .thenReturn(Collections.emptyIterator()); - when(clientMock.listConfigurationSettings(Mockito.any())).thenReturn(configurationListMock) - .thenReturn(configurationListMock); - FeatureSet featureSet = new FeatureSet(); - propertySource.initProperties(featureSet); - - String expectedKeyName = TEST_SLASH_KEY.replace('/', '.'); - String[] actualKeyNames = propertySource.getPropertyNames(); - - assertThat(actualKeyNames.length).isEqualTo(1); - assertThat(actualKeyNames[0]).isEqualTo(expectedKeyName); - assertThat(propertySource.getProperty(TEST_SLASH_KEY)).isNull(); - assertThat(propertySource.getProperty(expectedKeyName)).isEqualTo(TEST_SLASH_VALUE); - } - - @Test - public void testFeatureFlagCanBeInitedAndQueried() { - when(configurationListMock.iterator()).thenReturn(Collections.emptyIterator()) - .thenReturn(FEATURE_ITEMS.iterator()); - when(clientMock.listConfigurationSettings(Mockito.any())) - .thenReturn(configurationListMock).thenReturn(configurationListMock); - featureFlagStore.setEnabled(true); - - FeatureSet featureSet = new FeatureSet(); - try { - propertySource.initProperties(featureSet); - } catch (IOException e) { - fail("Failed Reading in Feature Flags"); - } - propertySource.initFeatures(featureSet); - - FeatureSet featureSetExpected = new FeatureSet(); - Feature feature = new Feature(); - feature.setKey("Alpha"); - HashMap filters = new HashMap<>(); - FeatureFlagFilter featureFlagFilter = new FeatureFlagFilter("TestFilter"); - filters.put(0, featureFlagFilter); - feature.setEnabledFor(filters); - Feature gamma = new Feature(); - gamma.setKey("Gamma"); - filters = new HashMap<>(); - featureFlagFilter = new FeatureFlagFilter("TestFilter"); - LinkedHashMap parameters = new LinkedHashMap<>(); - parameters.put("key", "value"); - featureFlagFilter.setParameters(parameters); - filters.put(0, featureFlagFilter); - gamma.setEnabledFor(filters); - featureSetExpected.addFeature("Alpha", feature); - featureSetExpected.addFeature("Beta", true); - featureSetExpected.addFeature("Gamma", gamma); - LinkedHashMap convertedValue = mapper.convertValue(featureSetExpected.getFeatureManagement(), - LinkedHashMap.class); - - assertEquals(convertedValue, propertySource.getProperty(FEATURE_MANAGEMENT_KEY)); - } - - @Test - public void testFeatureFlagDisabled() throws AppConfigurationStatusException, IOException { - when(configurationListMock.iterator()).thenReturn(Collections.emptyIterator()) - .thenReturn(FEATURE_ITEMS.iterator()); - when(clientMock.listConfigurationSettings(Mockito.any())) - .thenReturn(configurationListMock).thenReturn(configurationListMock); - featureFlagStore.setEnabled(false); - - FeatureSet featureSet = new FeatureSet(); - propertySource.initProperties(featureSet); - propertySource.initFeatures(featureSet); - - assertNull(propertySource.getProperty(FEATURE_MANAGEMENT_KEY)); - } - - @Test - public void testFeatureFlagThrowError() { - FeatureSet featureSet = new FeatureSet(); - when(configurationListMock.iterator()).thenReturn(Collections.emptyIterator()); - when(clientMock.listConfigurationSettings(Mockito.any())).thenReturn(configurationListMock); - try { - propertySource.initProperties(featureSet); - } catch (IOException e) { - assertEquals("Found Feature Flag /foo/test_key_1 with invalid Content Type of ", e.getMessage()); - } - } - - @Test - public void testFeatureFlagBuildError() { - featureFlagStore.setEnabled(true); - when(configurationListMock.iterator()).thenReturn(Collections.emptyIterator()) - .thenReturn(FEATURE_ITEMS.iterator()); - when(clientMock.listConfigurationSettings(Mockito.any())).thenReturn(configurationListMock); - - FeatureSet featureSet = new FeatureSet(); - try { - propertySource.initProperties(featureSet); - } catch (IOException e) { - fail(); - } - propertySource.initFeatures(featureSet); - - FeatureSet featureSetExpected = new FeatureSet(); - - HashMap filters = new HashMap<>(); - FeatureFlagFilter featureFlagFilter = new FeatureFlagFilter("TestFilter"); - - filters.put(0, featureFlagFilter); - - Feature alpha = new Feature(); - alpha.setKey("Alpha"); - alpha.setEnabledFor(filters); - - HashMap filters2 = new HashMap<>(); - FeatureFlagFilter featureFlagFilter2 = new FeatureFlagFilter("TestFilter"); - - filters2.put(0, featureFlagFilter2); - - LinkedHashMap parameters = new LinkedHashMap<>(); - parameters.put("key", "value"); - featureFlagFilter2.setParameters(parameters); - - Feature gamma = new Feature(); - gamma.setKey("Gamma"); - gamma.setEnabledFor(filters2); - filters2.put(0, featureFlagFilter2); - - featureSetExpected.addFeature("Alpha", alpha); - featureSetExpected.addFeature("Beta", true); - featureSetExpected.addFeature("Gamma", gamma); - LinkedHashMap convertedValue = mapper.convertValue(featureSetExpected.getFeatureManagement(), - LinkedHashMap.class); - - assertEquals(convertedValue, propertySource.getProperty(FEATURE_MANAGEMENT_KEY)); - } - - @Test - public void initNullValidContentTypeTest() throws AppConfigurationStatusException, IOException { - ArrayList items = new ArrayList<>(); - items.add(ITEM_NULL); - when(configurationListMock.iterator()).thenReturn(items.iterator()) - .thenReturn(Collections.emptyIterator()); - when(clientMock.listConfigurationSettings(Mockito.any())).thenReturn(configurationListMock); - - FeatureSet featureSet = new FeatureSet(); - propertySource.initProperties(featureSet); - - String[] keyNames = propertySource.getPropertyNames(); - String[] expectedKeyNames = items.stream() - .map(t -> t.getKey().substring(KEY_FILTER.length())).toArray(String[]::new); - - assertThat(keyNames).containsExactlyInAnyOrder(expectedKeyNames); - } - - @Test - public void initNullInvalidContentTypeFeatureFlagTest() throws AppConfigurationStatusException, IOException { - ArrayList items = new ArrayList<>(); - items.add(FEATURE_ITEM_NULL); - when(configurationListMock.iterator()).thenReturn(Collections.emptyIterator()) - .thenReturn(items.iterator()); - when(clientMock.listConfigurationSettings(Mockito.any())) - .thenReturn(configurationListMock).thenReturn(configurationListMock); - - FeatureSet featureSet = new FeatureSet(); - propertySource.initProperties(featureSet); - - String[] keyNames = propertySource.getPropertyNames(); - String[] expectedKeyNames = {}; - - assertThat(keyNames).containsExactlyInAnyOrder(expectedKeyNames); - } - - @Test - public void testFeatureFlagTargeting() throws AppConfigurationStatusException, IOException { - when(configurationListMock.iterator()).thenReturn(Collections.emptyIterator()) - .thenReturn(FEATURE_ITEMS_TARGETING.iterator()); - when(clientMock.listConfigurationSettings(Mockito.any())) - .thenReturn(configurationListMock).thenReturn(configurationListMock); - featureFlagStore.setEnabled(true); - - FeatureSet featureSet = new FeatureSet(); - propertySource.initProperties(featureSet); - propertySource.initFeatures(featureSet); - - FeatureSet featureSetExpected = new FeatureSet(); - Feature feature = new Feature(); - feature.setKey("target"); - HashMap filters = new HashMap<>(); - FeatureFlagFilter featureFlagFilter = new FeatureFlagFilter("targetingFilter"); - - LinkedHashMap parameters = new LinkedHashMap<>(); - - LinkedHashMap users = new LinkedHashMap<>(); - users.put("0", "Jeff"); - users.put("1", "Alicia"); - - LinkedHashMap groups = new LinkedHashMap<>(); - LinkedHashMap ring0 = new LinkedHashMap<>(); - LinkedHashMap ring1 = new LinkedHashMap<>(); - - ring0.put("name", "Ring0"); - ring0.put("rolloutPercentage", "100"); - - ring1.put("name", "Ring1"); - ring1.put("rolloutPercentage", "100"); - - groups.put("0", ring0); - groups.put("1", ring1); - - parameters.put(USERS, users); - parameters.put(GROUPS, groups); - parameters.put(DEFAULT_ROLLOUT_PERCENTAGE, 50); - - featureFlagFilter.setParameters(parameters); - filters.put(0, featureFlagFilter); - feature.setEnabledFor(filters); - - featureSetExpected.addFeature("target", feature); - LinkedHashMap convertedValue = mapper.convertValue(featureSetExpected.getFeatureManagement(), - LinkedHashMap.class); - - assertEquals(convertedValue.toString().length(), - propertySource.getProperty(FEATURE_MANAGEMENT_KEY).toString().length()); - } -} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/stores/KeyVaultClientTest.java b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/stores/KeyVaultClientTest.java deleted file mode 100644 index 3a8a44ca99cb5..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/stores/KeyVaultClientTest.java +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.config.stores; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.net.URI; -import java.net.URISyntaxException; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import com.azure.core.credential.TokenCredential; -import com.azure.security.keyvault.secrets.SecretAsyncClient; -import com.azure.security.keyvault.secrets.SecretClientBuilder; -import com.azure.security.keyvault.secrets.models.KeyVaultSecret; -import com.azure.spring.cloud.config.KeyVaultCredentialProvider; -import com.azure.spring.cloud.config.KeyVaultSecretProvider; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.resource.AppConfigManagedIdentityProperties; - -import reactor.core.publisher.Mono; - -public class KeyVaultClientTest { - - private KeyVaultClient clientStore; - - @Mock - private SecretClientBuilder builderMock; - - @Mock - private SecretAsyncClient clientMock; - - @Mock - private TokenCredential credentialMock; - - @Mock - private Mono monoSecret; - - private AppConfigurationProperties azureProperties; - - @BeforeEach - public void setup() { - MockitoAnnotations.openMocks(this); - } - - @AfterEach - public void cleanup() throws Exception { - MockitoAnnotations.openMocks(this).close(); - } - - @Test - public void multipleArguments() throws URISyntaxException { - azureProperties = new AppConfigurationProperties(); - AppConfigManagedIdentityProperties msiProps = new AppConfigManagedIdentityProperties(); - msiProps.setClientId("testclientid"); - azureProperties.setManagedIdentity(msiProps); - - String keyVaultUri = "https://keyvault.vault.azure.net/secrets/mySecret"; - - KeyVaultCredentialProvider provider = new KeyVaultCredentialProvider() { - - @Override - public TokenCredential getKeyVaultCredential(String uri) { - assertEquals("https://keyvault.vault.azure.net", uri); - return credentialMock; - } - }; - - clientStore = new KeyVaultClient(azureProperties, new URI(keyVaultUri), provider, null, null); - - KeyVaultClient test = Mockito.spy(clientStore); - Mockito.doReturn(builderMock).when(test).getBuilder(); - - Assertions.assertThrows(IllegalArgumentException.class, () -> test.build()); - } - - @Test - public void configProviderAuth() throws URISyntaxException { - azureProperties = new AppConfigurationProperties(); - azureProperties.setManagedIdentity(null); - - String keyVaultUri = "https://keyvault.vault.azure.net/secrets/mySecret"; - - KeyVaultCredentialProvider provider = new KeyVaultCredentialProvider() { - - @Override - public TokenCredential getKeyVaultCredential(String uri) { - assertEquals("https://keyvault.vault.azure.net", uri); - return credentialMock; - } - }; - - clientStore = new KeyVaultClient(azureProperties, new URI(keyVaultUri), provider, null, null); - - KeyVaultClient test = Mockito.spy(clientStore); - Mockito.doReturn(builderMock).when(test).getBuilder(); - - when(builderMock.vaultUrl(Mockito.any())).thenReturn(builderMock); - when(builderMock.buildAsyncClient()).thenReturn(clientMock); - - test.build(); - - when(clientMock.getSecret(Mockito.any(), Mockito.any())) - .thenReturn(monoSecret); - when(monoSecret.block(Mockito.any())).thenReturn(new KeyVaultSecret("", "")); - - assertNotNull(test.getSecret(new URI(keyVaultUri), 10)); - assertEquals(test.getSecret(new URI(keyVaultUri), 10).getName(), ""); - } - - @Test - public void configClientIdAuth() throws URISyntaxException { - azureProperties = new AppConfigurationProperties(); - AppConfigManagedIdentityProperties msiProps = new AppConfigManagedIdentityProperties(); - msiProps.setClientId("testClientId"); - AppConfigManagedIdentityProperties test2 = Mockito.spy(msiProps); - azureProperties.setManagedIdentity(test2); - - String keyVaultUri = "https://keyvault.vault.azure.net/secrets/mySecret"; - - clientStore = new KeyVaultClient(azureProperties, new URI(keyVaultUri), null, null, null); - - KeyVaultClient test = Mockito.spy(clientStore); - Mockito.doReturn(builderMock).when(test).getBuilder(); - - when(builderMock.vaultUrl(Mockito.any())).thenReturn(builderMock); - when(builderMock.buildAsyncClient()).thenReturn(clientMock); - - test.build(); - - when(clientMock.getSecret(Mockito.any(), Mockito.any())) - .thenReturn(monoSecret); - when(monoSecret.block(Mockito.any())).thenReturn(new KeyVaultSecret("", "")); - - assertNotNull(test.getSecret(new URI(keyVaultUri), 10)); - assertEquals(test.getSecret(new URI(keyVaultUri), 10).getName(), ""); - - verify(test2, times(2)).getClientId(); - } - - @Test - public void systemAssignedCredentials() throws URISyntaxException { - azureProperties = new AppConfigurationProperties(); - AppConfigManagedIdentityProperties msiProps = new AppConfigManagedIdentityProperties(); - msiProps.setClientId(""); - AppConfigManagedIdentityProperties test2 = Mockito.spy(msiProps); - azureProperties.setManagedIdentity(test2); - - String keyVaultUri = "https://keyvault.vault.azure.net/secrets/mySecret"; - - clientStore = new KeyVaultClient(azureProperties, new URI(keyVaultUri), null, null, null); - - KeyVaultClient test = Mockito.spy(clientStore); - Mockito.doReturn(builderMock).when(test).getBuilder(); - - when(builderMock.vaultUrl(Mockito.any())).thenReturn(builderMock); - when(builderMock.buildAsyncClient()).thenReturn(clientMock); - - test.build(); - - when(clientMock.getSecret(Mockito.any(), Mockito.any())) - .thenReturn(monoSecret); - when(monoSecret.block(Mockito.any())).thenReturn(new KeyVaultSecret("", "")); - - assertNotNull(test.getSecret(new URI(keyVaultUri), 10)); - assertEquals(test.getSecret(new URI(keyVaultUri), 10).getName(), ""); - - verify(test2, times(1)).getClientId(); - } - - @Test - public void secretResolverTest() throws URISyntaxException { - azureProperties = new AppConfigurationProperties(); - - String keyVaultUri = "https://keyvault.vault.azure.net/secrets/mySecret"; - - clientStore = new KeyVaultClient(azureProperties, new URI(keyVaultUri), null, null, new TestSecretResolver()); - - KeyVaultClient test = Mockito.spy(clientStore); - Mockito.doReturn(builderMock).when(test).getBuilder(); - - when(builderMock.vaultUrl(Mockito.any())).thenReturn(builderMock); - - assertEquals("Test-Value", test.getSecret(new URI(keyVaultUri + "/testSecret"), 10).getValue()); - assertEquals("Default-Secret", test.getSecret(new URI(keyVaultUri + "/testSecret2"), 10).getValue()); - } - - class TestSecretResolver implements KeyVaultSecretProvider { - - @Override - public String getSecret(String uri) { - if (uri.endsWith("/testSecret")) { - return "Test-Value"; - } - return "Default-Secret"; - } - - } -} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/resources/META-INF/spring.factories b/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 02418665328dd..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -com.azure.spring.cloud.feature.manager.FeatureManagementWebConfiguration \ No newline at end of file diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/README.md b/sdk/appconfiguration/azure-spring-cloud-feature-management/README.md deleted file mode 100644 index 43f481ab7c533..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/README.md +++ /dev/null @@ -1,277 +0,0 @@ -# Spring Cloud for Azure feature management client library for Java - -## Key concepts - -### Feature Management - -Feature flags provide a way for Spring Boot applications to turn features on or off dynamically. Developers can use feature flags in simple use cases like conditional statement to more advanced scenarios like conditionally adding routes. Feature Flags are not dependent of any spring-cloud-azure dependencies, but may be used in conjunction with spring-cloud-azure-appconfiguration-config. - -Here are some of the benefits of using this library: - -* A common convention for feature management -* Low barrier-to-entry - * Supports application.yml file feature flag setup -* Feature Flag lifetime management - * Configuration values can change in real-time, feature flags can be consistent across the entire request - -### Feature Flags - -Feature flags are composed of two parts, a name and a list of feature-filters that are used to turn the feature on. - -### Feature Filters - -Feature filters define a scenario for when a feature should be enabled. When a feature is evaluated for whether it is on or off, its list of feature-filters are traversed until one of the filters decides the feature should be enabled. At this point the feature is considered enabled and traversal through the feature filters stops. If no feature filter indicates that the feature should be enabled, then it will be considered disabled. - -As an example, a Microsoft Edge browser feature filter could be designed. This feature filter would activate any features it is attached to as long as an HTTP request is coming from Microsoft Edge. - -### Registration - -The Spring Configuration system is used to determine the state of feature flags. Any system can be used to have them read in, such as application.yml, spring-cloud-azure-appconfiguration-config and more. - -### Feature Flag Declaration - -The feature management library supports application.yml or bootstrap.yml as a feature flag source. Below we have an example of the format used to set up feature flags in a application.yml file. - -```yaml -feature-management: - feature-t: false - feature-u: - enabled-for: - - - name: Random - feature-v: - enabled-for: - - - name: TimeWindowFilter - parameters: - time-window-filter-setting-start: "Wed, 01 May 2019 13:59:59 GMT" - time-window-filter-setting-end: "Mon, 01 July 2019 00:00:00 GMT" - feature-w: - evaluate: false - enabled-for: - - - name: AlwaysOnFilter -``` - -The `feature-management` section of the YAML document is used by convention to load feature flags. In the section above, we see that we have provided three different features. Features define their filters using the `enabled-for` property. We can see that feature `feature-t` is set to false with no filters set. `feature-t` will always return false, this can also be done for true. `feature-u` which has only one feature filter `Random` which does not require any configuration so it only has the name property. `feature-v` it specifies a feature filter named `TimeWindow`. This is an example of a configurable feature filter. We can see in the example that the filter has a parameter's property. This is used to configure the filter. In this case, the start and end times for the feature to be active are configured. - -The `AlwaysOnFilter` is a Filter that always evaluates as `true`. This filter can be used to turn this feature flag on, without removing the other feature filters. The `evaluate` field is used to stop the evaluation of the feature filters, and results in the feature flag to always return `false`. - -#### Supported properties - -Name | Description | Required | Default ----|---|---|--- -spring.cloud.azure.feature.management.fail-fast | Whether throw RuntimeException or not when exception occurs | No | true - -### Consumption - -The simplest use case for feature flags is to do a conditional check for whether a feature is enabled to take different paths in code. The use cases grow when additional using spring-cloud-azure-feature-flag-web to manage web based features. - -#### Feature Check - -The basic form of feature management is checking if a feature is enabled and then performing actions based on the result. This is done through the autowiring `FeatureManager` and calling it's `isEnabledAsync` method. - -```java -@Autowired -FeatureManager featureManager; - -if(featureManager.isEnabledAsync("feature-t").block()) { - // Do Something -} -``` - -`FeatureManager` can also be accessed by `@Component` classes. - -#### Controllers - -When using the Feature Management Web library you can require that a given feature is enabled in order to execute. This can be done by using the `@FeatureOn` annotation. - -```java -@GetMapping("/featureT") -@FeatureGate(feature = "feature-t") -@ResponseBody -public String featureT() { - ... -} -``` - -The `featureT` endpoint can only be accessed if "feature-t" is enabled. - -#### Disabled Action Handling - -When a controller is blocked because the feature it specifies is disabled, `IDisabledFeaturesHandler` will be invoked. By default, a HTTP 404 is returned. This can be overridden using implementing `IDisabledFeaturesHandler`. - -```java -@Component -public class DisabledFeaturesHandler implements IDisabledFeaturesHandler{ - - @Override - public HttpServletResponse handleDisabledFeatures(HttpServletRequest request, HttpServletResponse response) { - ... - return response; - } - -} -``` - -#### Routing - -Certain routes may expose application capabilites that are gated by features. These routes can redirected if a feature is disabled to another endpoint. - -```java -@GetMapping("/featureT") -@FeatureGate(feature = "feature-t" fallback= "/oldEndpoint") -@ResponseBody -public String featureT() { - ... -} - -@GetMapping("/oldEndpoint") -@ResponseBody -public String oldEndpoint() { - ... -} -``` - -### Implementing a Feature Filter - -Creating a feature filter provides a way to enable features based on criteria that you define. To implement a feature filter, the `FeatureFilter` interface must be implemented. `FeatureFilter` has a single method `evaluate`. When a feature specifies that it can be enabled with a feature filter, the `evaluate` method is called. If `evaluate` returns `true` it means the feature should be enabled. If `false` it will continue evaluating the Feature's filters until one returns true. If all return `false` then the feature is off. - -Feature filters are found by being defined as `@Component` where there name matches the expected filter defined in the configuration. - -```java -@Component("Random") -public class Random implements FeatureFilter { - - @Override - public boolean evaluate(FeatureFilterEvaluationContext context) { - double chance = Double.valueOf((String) context.getParameters().get("chance")); - return Math.random() > chance/100; - } - -} -``` - -#### Parameterized Feature Filters - -Some feature filters require parameters to decide whether a feature should be turned on or not. For example a browser feature filter may turn on a feature for a certain set of browsers. It may be desired that Edge and Chrome browsers enable a feature, while FireFox does not. To do this a feature filter can be designed to expect parameters. These parameters would be specified in the feature configuration, and in code would be accessible via the `FeatureFilterEvaluationContext` parameter of `evaluate`. `FeatureFilterEvaluationContext` has a property `parameters` which is a `HashMap`. - -### Request Based Features/Snapshot - -There are scenarios which require the state of a feature to remain consistent during the lifetime of a request. The values returned from the standard `FeatureManager` may change if the configuration source which it is pulling from is updated during the request. This can be prevented by using `FeatureManagerSnapshot` and `@FeatureOn( snapshot = true )`. `FeatureManagerSnapshot` can be retrieved in the same manner as `FeatureManager`. `FeatureManagerSnapshot` calls `FeatureManager`, but it caches the first evaluated state of a feature during a request and will return the same state of a feature during its lifetime. - -### Built-In Feature Filters - -There are a few feature filters that come with the `azure-spring-cloud-feature-management` package. These feature filters are not added automatically, but can be referenced and registered as soon as the package is registered. - -Each of the built-in feature filters have their own parameters. Here is the list of feature filters along with examples. - -#### PercentageFilter - -This filter provides the capability to enable a feature based on a set percentage. - -```yaml -feature-management: - feature-v: - enabled-for: - - - name: PercentageFilter - parameters: - percentage-filter-setting: 50 -``` - -#### TimeWindowFilter - -This filter provides the capability to enable a feature based on a time window. If only `time-window-filter-setting-end` is specified, the feature will be considered on until that time. If only start is specified, the feature will be considered on at all points after that time. If both are specified the feature will be considered valid between the two times. - -```yaml -feature-management: - feature-v: - enabled-for: - - - name: TimeWindowFilter - parameters: - time-window-filter-setting-start: "Wed, 01 May 2019 13:59:59 GMT", - time-window-filter-setting-end: "Mon, 01 July 2019 00:00:00 GMT" -``` - -#### TargetingFilter - -This filter provides the capability to enable a feature for a target audience. An in-depth explanation of targeting is explained in the targeting section below. The filter parameters include an audience object which describes users, groups, and a default percentage of the user base that should have access to the feature. Each group object that is listed in the target audience must also specify what percentage of the group's members should have access. If a user is specified in the users section directly, or if the user is in the included percentage of any of the group rollouts, or if the user falls into the default rollout percentage then that user will have the feature enabled. - -```yml -feature-management: - target: - enabled-for: - - - name: targetingFilter - parameters: - users: - - Jeff - - Alicia - groups: - - - name: Ring0 - rolloutPercentage: 100 - - - name: Ring1 - rolloutPercentage: 100 - defaultRolloutPercentage: 50 -``` - -### Targeting - -Targeting is a feature management strategy that enables developers to progressively roll out new features to their user base. The strategy is built on the concept of targeting a set of users known as the target audience. An audience is made up of specific users, groups, and a designated percentage of the entire user base. The groups that are included in the audience can be broken down further into percentages of their total members. - -The following steps demonstrate an example of a progressive rollout for a new 'Beta' feature: - -1. Individual users Jeff and Alicia are granted access to the Beta -1. Another user, Mark, asks to opt-in and is included. -1. Twenty percent of a group known as "Ring1" users are included in the Beta. -1. The number of "Ring1" users included in the beta is bumped up to 100 percent. -1. Five percent of the user base is included in the beta. -1. The rollout percentage is bumped up to 100 percent and the feature is completely rolled out. -1. This strategy for rolling out a feature is built in to the library through the included TargetingFilter feature filter. - -#### Targeting in an Application - -An example web application that uses the targeting feature filter is available in the [example project][example_project]. - -To begin using the `TargetingFilter` in an application it must be added as a `@Bean` like any other Feature Filter. `TargetingFilter` relies on another `@Bean` to be added to the application, `ITargetingContextAccessor`. The `ITargetingContextAccessor` allows for defining the current `TargetingContext` to be used for defining the current user id and groups. An example of this is: - -```java -public class TargetingContextAccessor implements ITargetingContextAccessor { - - @Override - public Mono getContextAsync() { - TargetingContext context = new TargetingContext(); - context.setUserId("Jeff"); - ArrayList groups = new ArrayList(); - groups.add("Ring0"); - context.setGroups(groups); - return Mono.just(context); - } - -} -``` - -#### Targeting Evaluation Options - -Options are available to customize how targeting evaluation is performed across a given `TargetingFilter`. An optional parameter `TargetingEvaluationOptions` can be set during `TargetingFilter` creation. - -```java - @Bean - public TargetingFilter targetingFilter(ITargetingContextAccessor contextAccessor) { - return new TargetingFilter(contextAccessor, new TargetingEvaluationOptions().setIgnoreCase(true)); - } -``` - -## Getting started -## Key concepts -## Examples -## Troubleshooting -## Next steps -## Contributing - - -[example_project]: https://github.com/Azure-Samples/azure-spring-boot-samples/tree/spring-cloud-azure_v4.3.0/appconfiguration/azure-spring-cloud-feature-management-web/azure-spring-cloud-feature-management-web-sample diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java b/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java deleted file mode 100644 index a9b224a71cf94..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager; - -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Configuration for setting up FeatureManager - */ -@Configuration -@EnableConfigurationProperties({FeatureManagementConfigProperties.class}) -public class FeatureManagementConfiguration { - - /** - * Creates Feature Manager - * @param properties Feature Management configuration properties - * @return FeatureManager - */ - @Bean - public FeatureManager featureManager(FeatureManagementConfigProperties properties) { - return new FeatureManager(properties); - } - -} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManager.java b/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManager.java deleted file mode 100644 index 2bbb9da628924..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManager.java +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.stereotype.Component; -import org.springframework.util.ReflectionUtils; - -import com.azure.spring.cloud.feature.manager.entities.Feature; -import com.azure.spring.cloud.feature.manager.entities.FeatureFilterEvaluationContext; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; - -import reactor.core.publisher.Mono; - -/** - * Holds information on Feature Management properties and can check if a given feature is enabled. - */ -@Component("FeatureManagement") -@ConfigurationProperties(prefix = "feature-management") -public class FeatureManager extends HashMap { - - private static final Logger LOGGER = LoggerFactory.getLogger(FeatureManager.class); - - private static final long serialVersionUID = -5941681857165566018L; - - @Autowired - private transient ApplicationContext context; - - private transient FeatureManagementConfigProperties properties; - - private transient Map featureManagement; - - /** - * Holds FeatureFlags that are either enabled or disabled. - */ - private Map onOff; - - private static final ObjectMapper MAPPER = new ObjectMapper() - .setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE); - - /** - * Used to evaluate whether a feature is enabled or disabled. - * @param properties Configuration options for Feature Management - */ - public FeatureManager(FeatureManagementConfigProperties properties) { - this.properties = properties; - featureManagement = new HashMap<>(); - onOff = new HashMap<>(); - } - - /** - * Checks to see if the feature is enabled. If enabled it check each filter, once a single filter returns true it - * returns true. If no filter returns true, it returns false. If there are no filters, it returns true. If feature - * isn't found it returns false. - * - * @param feature Feature being checked. - * @return state of the feature - * @throws FilterNotFoundException file not found - */ - public Mono isEnabledAsync(String feature) throws FilterNotFoundException { - return Mono.just(checkFeatures(feature)); - } - - private boolean checkFeatures(String feature) throws FilterNotFoundException { - if (featureManagement == null || onOff == null) { - return false; - } - - Boolean boolFeature = onOff.get(feature); - - if (boolFeature != null) { - return boolFeature; - } - - Feature featureItem = featureManagement.get(feature); - if (featureItem == null || !featureItem.getEvaluate()) { - return false; - } - - return featureItem.getEnabledFor().values().stream().filter(Objects::nonNull) - .filter(featureFilter -> featureFilter.getName() != null) - .map(featureFilter -> isFeatureOn(featureFilter, feature)).findAny().orElse(false); - } - - private boolean isFeatureOn(FeatureFilterEvaluationContext filter, String feature) { - try { - FeatureFilter featureFilter = (FeatureFilter) context.getBean(filter.getName()); - filter.setFeatureName(feature); - - return featureFilter.evaluate(filter); - } catch (NoSuchBeanDefinitionException e) { - LOGGER.error("Was unable to find Filter {}. Does the class exist and set as an @Component?", - filter.getName()); - if (properties.isFailFast()) { - String message = "Fail fast is set and a Filter was unable to be found"; - ReflectionUtils.rethrowRuntimeException(new FilterNotFoundException(message, e, filter)); - } - } - return false; - } - - @SuppressWarnings("unchecked") - private void addToFeatures(Map features, String key, String combined) { - Object featureKey = features.get(key); - if (!combined.isEmpty() && !combined.endsWith(".")) { - combined += "."; - } - if (featureKey instanceof Boolean) { - onOff.put(combined + key, (Boolean) featureKey); - } else { - Feature feature = null; - try { - feature = MAPPER.convertValue(featureKey, Feature.class); - } catch (IllegalArgumentException e) { - LOGGER.error("Found invalid feature {} with value {}.", combined + key, featureKey.toString()); - } - - // When coming from a file "feature.flag" is not a possible flag name - if (feature != null && feature.getEnabledFor() == null && feature.getKey() == null) { - if (LinkedHashMap.class.isAssignableFrom(featureKey.getClass())) { - features = (LinkedHashMap) featureKey; - for (String fKey : features.keySet()) { - addToFeatures(features, fKey, combined + key); - } - } - } else { - if (feature != null) { - feature.setKey(key); - featureManagement.put(key, feature); - } - } - } - } - - @Override - @SuppressWarnings("unchecked") - public void putAll(Map m) { - if (m == null) { - return; - } - - // Need to reset or switch between on/off to conditional doesn't work - featureManagement = new HashMap<>(); - onOff = new HashMap<>(); - - if (m.size() == 1 && m.containsKey("featureManagement")) { - m = (Map) m.get("featureManagement"); - } - - for (String key : m.keySet()) { - addToFeatures(m, key, ""); - } - } - - /** - * Returns the names of all features flags - * - * @return a set of all feature names - */ - public Set getAllFeatureNames() { - Set allFeatures = new HashSet<>(); - - allFeatures.addAll(onOff.keySet()); - allFeatures.addAll(featureManagement.keySet()); - return allFeatures; - } - - /** - * @return the featureManagement - */ - Map getFeatureManagement() { - return featureManagement; - } - - /** - * @return the onOff - */ - Map getOnOff() { - return onOff; - } - -} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/AlwaysOnFilter.java b/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/AlwaysOnFilter.java deleted file mode 100644 index 751703860d18e..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/AlwaysOnFilter.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.azure.spring.cloud.feature.manager.feature.filters; - -import com.azure.spring.cloud.feature.manager.FeatureFilter; -import com.azure.spring.cloud.feature.manager.entities.FeatureFilterEvaluationContext; - -/** - * A filter that always returns true - */ -public class AlwaysOnFilter implements FeatureFilter { - - @Override - public boolean evaluate(FeatureFilterEvaluationContext context) { - return true; - } - -} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/FeatureManagerTest.java b/sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/FeatureManagerTest.java deleted file mode 100644 index c2b6f1282a3c8..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/FeatureManagerTest.java +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.when; - -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.concurrent.ExecutionException; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.ApplicationContext; - -import com.azure.spring.cloud.feature.manager.entities.Feature; -import com.azure.spring.cloud.feature.manager.entities.FeatureFilterEvaluationContext; -import com.azure.spring.cloud.feature.manager.feature.filters.AlwaysOnFilter; - -/** - * Unit tests for FeatureManager. - */ -@SpringBootTest(classes = { TestConfiguration.class, SpringBootTest.class }) -public class FeatureManagerTest { - - private static final String FEATURE_KEY = "TestFeature"; - - private static final String FILTER_NAME = "Filter1"; - - private static final String PARAM_1_NAME = "param1"; - - private static final String PARAM_1_VALUE = "testParam"; - - @InjectMocks - private FeatureManager featureManager; - - @Mock - private ApplicationContext context; - - @Mock - private FeatureManagementConfigProperties properties; - - @BeforeEach - public void setup() { - MockitoAnnotations.openMocks(this); - when(properties.isFailFast()).thenReturn(true); - } - - @AfterEach - public void cleanup() throws Exception { - MockitoAnnotations.openMocks(this).close(); - } - - /** - * Tests the conversion that takes place when data comes from EnumerablePropertySource. - */ - @Test - public void loadFeatureManagerWithLinkedHashSet() { - Feature f = new Feature(); - f.setKey(FEATURE_KEY); - - LinkedHashMap testMap = new LinkedHashMap(); - LinkedHashMap testFeature = new LinkedHashMap(); - LinkedHashMap enabledFor = new LinkedHashMap(); - LinkedHashMap ffec = new LinkedHashMap(); - LinkedHashMap parameters = new LinkedHashMap(); - ffec.put("name", FILTER_NAME); - parameters.put(PARAM_1_NAME, PARAM_1_VALUE); - ffec.put("parameters", parameters); - enabledFor.put("0", ffec); - testFeature.put("enabled-for", enabledFor); - testMap.put(f.getKey(), testFeature); - - featureManager.putAll(testMap); - assertNotNull(featureManager); - assertNotNull(featureManager.getFeatureManagement()); - assertEquals(1, featureManager.getFeatureManagement().size()); - assertNotNull(featureManager.getFeatureManagement().get(FEATURE_KEY)); - Feature feature = featureManager.getFeatureManagement().get(FEATURE_KEY); - assertEquals(FEATURE_KEY, feature.getKey()); - assertEquals(1, feature.getEnabledFor().size()); - FeatureFilterEvaluationContext zeroth = feature.getEnabledFor().get(0); - assertEquals(FILTER_NAME, zeroth.getName()); - assertEquals(1, zeroth.getParameters().size()); - assertEquals(PARAM_1_VALUE, zeroth.getParameters().get(PARAM_1_NAME)); - } - - @Test - public void isEnabledFeatureNotFound() throws InterruptedException, ExecutionException, FilterNotFoundException { - assertFalse(featureManager.isEnabledAsync("Non Existed Feature").block()); - } - - @Test - public void isEnabledFeatureOff() throws InterruptedException, ExecutionException, FilterNotFoundException { - HashMap features = new HashMap(); - features.put("Off", false); - featureManager.putAll(features); - - assertFalse(featureManager.isEnabledAsync("Off").block()); - } - - @Test - public void isEnabledFeatureHasNoFilters() - throws InterruptedException, ExecutionException, FilterNotFoundException { - HashMap features = new HashMap(); - Feature noFilters = new Feature(); - noFilters.setKey("NoFilters"); - noFilters.setEnabledFor(new HashMap()); - features.put("NoFilters", noFilters); - featureManager.putAll(features); - - assertFalse(featureManager.isEnabledAsync("NoFilters").block()); - } - - @Test - public void isEnabledON() throws InterruptedException, ExecutionException, FilterNotFoundException { - HashMap features = new HashMap(); - Feature onFeature = new Feature(); - onFeature.setKey("On"); - HashMap filters = new HashMap(); - FeatureFilterEvaluationContext alwaysOn = new FeatureFilterEvaluationContext(); - alwaysOn.setName("AlwaysOn"); - filters.put(0, alwaysOn); - onFeature.setEnabledFor(filters); - features.put("On", onFeature); - featureManager.putAll(features); - - when(context.getBean(Mockito.matches("AlwaysOn"))).thenReturn(new AlwaysOnFilter()); - - assertTrue(featureManager.isEnabledAsync("On").block()); - } - - @Test - public void isEnabledPeriodSplit() throws InterruptedException, ExecutionException, FilterNotFoundException { - LinkedHashMap features = new LinkedHashMap(); - LinkedHashMap featuresOn = new LinkedHashMap(); - - featuresOn.put("A", true); - features.put("Beta", featuresOn); - - featureManager.putAll(features); - - assertTrue(featureManager.isEnabledAsync("Beta.A").block()); - } - - @Test - public void isEnabledInvalid() throws InterruptedException, ExecutionException, FilterNotFoundException { - LinkedHashMap features = new LinkedHashMap(); - LinkedHashMap featuresOn = new LinkedHashMap(); - - featuresOn.put("A", 5); - features.put("Beta", featuresOn); - - featureManager.putAll(features); - - assertFalse(featureManager.isEnabledAsync("Beta.A").block()); - assertEquals(0, featureManager.size()); - } - - @Test - public void isEnabledOnBoolean() throws InterruptedException, ExecutionException, FilterNotFoundException { - HashMap features = new HashMap(); - features.put("On", true); - featureManager.putAll(features); - - assertTrue(featureManager.isEnabledAsync("On").block()); - } - - @Test - public void featureManagerNotEnabledCorrectly() - throws InterruptedException, ExecutionException, FilterNotFoundException { - FeatureManager featureManager = new FeatureManager(null); - assertFalse(featureManager.isEnabledAsync("").block()); - } - - @Test - public void bootstrapConfiguration() { - HashMap features = new HashMap(); - features.put("FeatureU", false); - Feature featureV = new Feature(); - HashMap filterMapper = new HashMap(); - - FeatureFilterEvaluationContext enabledFor = new FeatureFilterEvaluationContext(); - enabledFor.setName("Random"); - - LinkedHashMap parameters = new LinkedHashMap(); - parameters.put("chance", "50"); - - enabledFor.setParameters(parameters); - filterMapper.put(0, enabledFor); - featureV.setEnabledFor(filterMapper); - features.put("FeatureV", featureV); - featureManager.putAll(features); - - assertNotNull(featureManager.getOnOff()); - assertNotNull(featureManager.getFeatureManagement()); - - assertEquals(featureManager.getOnOff().get("FeatureU"), false); - Feature feature = featureManager.getFeatureManagement().get("FeatureV"); - assertEquals(feature.getEnabledFor().size(), 1); - FeatureFilterEvaluationContext ffec = feature.getEnabledFor().get(0); - assertEquals(ffec.getName(), "Random"); - assertEquals(ffec.getParameters().size(), 1); - assertEquals(ffec.getParameters().get("chance"), "50"); - assertEquals(2, featureManager.getAllFeatureNames().size()); - } - - @Test - public void disabledEvaluate() { - HashMap features = new HashMap(); - Feature featureV = new Feature(); - HashMap filterMapper = new HashMap(); - - FeatureFilterEvaluationContext enabledFor = new FeatureFilterEvaluationContext(); - enabledFor.setName("AlwaysOn"); - enabledFor.setParameters(new LinkedHashMap()); - - filterMapper.put(0, enabledFor); - featureV.setEnabledFor(filterMapper); - featureV.setEvaluate(false); - features.put("FeatureV", featureV); - featureManager.putAll(features); - - assertNotNull(featureManager.getOnOff()); - assertNotNull(featureManager.getFeatureManagement()); - - Feature feature = featureManager.getFeatureManagement().get("FeatureV"); - assertEquals(feature.getEnabledFor().size(), 1); - FeatureFilterEvaluationContext ffec = feature.getEnabledFor().get(0); - assertEquals(ffec.getName(), "AlwaysOn"); - assertEquals(ffec.getParameters().size(), 0); - assertEquals(1, featureManager.getAllFeatureNames().size()); - assertFalse(featureManager.isEnabledAsync("FeatureV").block()); - } - - @Test - public void noFilter() throws FilterNotFoundException { - HashMap features = new HashMap(); - Feature onFeature = new Feature(); - onFeature.setKey("Off"); - HashMap filters = new HashMap(); - FeatureFilterEvaluationContext alwaysOn = new FeatureFilterEvaluationContext(); - alwaysOn.setName("AlwaysOff"); - filters.put(0, alwaysOn); - onFeature.setEnabledFor(filters); - features.put("Off", onFeature); - featureManager.putAll(features); - - when(context.getBean(Mockito.matches("AlwaysOff"))).thenThrow(new NoSuchBeanDefinitionException("")); - - FilterNotFoundException e = assertThrows(FilterNotFoundException.class, - () -> featureManager.isEnabledAsync("Off").block()); - assertThat(e).hasMessage("Fail fast is set and a Filter was unable to be found: AlwaysOff"); - } - -} diff --git a/sdk/appconfiguration/ci.yml b/sdk/appconfiguration/ci.yml index b4af603483bd5..b0e2be75a78c5 100644 --- a/sdk/appconfiguration/ci.yml +++ b/sdk/appconfiguration/ci.yml @@ -12,23 +12,11 @@ trigger: - sdk/appconfiguration/azure-data-appconfiguration/ - sdk/appconfiguration/azure-data-appconfiguration-perf/ - sdk/appconfiguration/azure-resourcemanager-appconfiguration/ - - sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/ - - sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/ - - sdk/appconfiguration/azure-spring-cloud-feature-management/ - - sdk/appconfiguration/azure-spring-cloud-feature-management-web/ - - sdk/appconfiguration/azure-spring-cloud-starter-appconfiguration-config/ - - sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/ exclude: - sdk/appconfiguration/pom.xml - sdk/appconfiguration/azure-data-appconfiguration/pom.xml - sdk/appconfiguration/azure-data-appconfiguration-perf/pom.xml - sdk/appconfiguration/azure-resourcemanager-appconfiguration/pom.xml - - sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/pom.xml - - sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/pom.xml - - sdk/appconfiguration/azure-spring-cloud-feature-management/pom.xml - - sdk/appconfiguration/azure-spring-cloud-feature-management-web/pom.xml - - sdk/appconfiguration/azure-spring-cloud-starter-appconfiguration-config/pom.xml - - sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/pom.xml pr: branches: @@ -43,53 +31,21 @@ pr: - sdk/appconfiguration/azure-data-appconfiguration/ - sdk/appconfiguration/azure-data-appconfiguration-perf/ - sdk/appconfiguration/azure-resourcemanager-appconfiguration/ - - sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/ - - sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/ - - sdk/appconfiguration/azure-spring-cloud-feature-management/ - - sdk/appconfiguration/azure-spring-cloud-feature-management-web/ - - sdk/appconfiguration/azure-spring-cloud-starter-appconfiguration-config/ - - sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/ exclude: - sdk/appconfiguration/pom.xml - sdk/appconfiguration/azure-data-appconfiguration/pom.xml - sdk/appconfiguration/azure-data-appconfiguration-perf/pom.xml - sdk/appconfiguration/azure-resourcemanager-appconfiguration/pom.xml - - sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/pom.xml - - sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/pom.xml - - sdk/appconfiguration/azure-spring-cloud-feature-management/pom.xml - - sdk/appconfiguration/azure-spring-cloud-feature-management-web/pom.xml - - sdk/appconfiguration/azure-spring-cloud-starter-appconfiguration-config/pom.xml - - sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/pom.xml parameters: -- name: release_azuredataappconfiguration - displayName: 'azure-data-appconfiguration' - type: boolean - default: true -- name: release_azurespringcloudappconfigurationconfig - displayName: 'azure-spring-cloud-appconfiguration-config' - type: boolean - default: true -- name: release_azurespringcloudappconfigurationconfigweb - displayName: 'azure-spring-cloud-appconfiguration-config-web' - type: boolean - default: true -- name: release_azurespringcloudfeaturemanagement - displayName: 'azure-spring-cloud-feature-management' - type: boolean - default: true -- name: release_azurespringcloudfeaturemanagementweb - displayName: 'azure-spring-cloud-feature-management-web' - type: boolean - default: true -- name: release_azurespringcloudstarterappconfigurationconfig - displayName: 'azure-spring-cloud-starter-appconfiguration-config' - type: boolean - default: true -- name: release_azureresourcemanagerappconfiguration - displayName: 'azure-resourcemanager-appconfiguration' - type: boolean - default: false + - name: release_azuredataappconfiguration + displayName: "azure-data-appconfiguration" + type: boolean + default: true + - name: release_azureresourcemanagerappconfiguration + displayName: "azure-resourcemanager-appconfiguration" + type: boolean + default: false extends: template: ../../eng/pipelines/templates/stages/archetype-sdk-client.yml @@ -101,31 +57,7 @@ extends: groupId: com.azure safeName: azuredataappconfiguration releaseInBatch: ${{ parameters.release_azuredataappconfiguration }} - - name: azure-spring-cloud-appconfiguration-config - groupId: com.azure.spring - safeName: azurespringcloudappconfigurationconfig - releaseInBatch: ${{ parameters.release_azurespringcloudappconfigurationconfig }} - - name: azure-spring-cloud-appconfiguration-config-web - groupId: com.azure.spring - safeName: azurespringcloudappconfigurationconfigweb - releaseInBatch: ${{ parameters.release_azurespringcloudappconfigurationconfigweb }} - - name: azure-spring-cloud-feature-management - groupId: com.azure.spring - safeName: azurespringcloudfeaturemanagement - releaseInBatch: ${{ parameters.release_azurespringcloudfeaturemanagement }} - - name: azure-spring-cloud-feature-management-web - groupId: com.azure.spring - safeName: azurespringcloudfeaturemanagementweb - releaseInBatch: ${{ parameters.release_azurespringcloudfeaturemanagementweb }} - - name: azure-spring-cloud-starter-appconfiguration-config - groupId: com.azure.spring - safeName: azurespringcloudstarterappconfigurationconfig - releaseInBatch: ${{ parameters.release_azurespringcloudstarterappconfigurationconfig }} - name: azure-resourcemanager-appconfiguration groupId: com.azure.resourcemanager safeName: azureresourcemanagerappconfiguration releaseInBatch: ${{ parameters.release_azureresourcemanagerappconfiguration }} - AdditionalModules: - - name: azure-spring-cloud-test-appconfiguration-config - groupId: com.azure.spring - safeName: azurespringcloudtestappconfigurationconfig \ No newline at end of file diff --git a/sdk/appconfiguration/pom.xml b/sdk/appconfiguration/pom.xml index 8e88ca6ca5991..4561fae7b0afc 100644 --- a/sdk/appconfiguration/pom.xml +++ b/sdk/appconfiguration/pom.xml @@ -1,22 +1,16 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 com.azure azure-appconfiguration-service pom 1.0.0 - azure-data-appconfiguration - azure-data-appconfiguration-perf - azure-resourcemanager-appconfiguration - azure-spring-cloud-test-appconfiguration-config - azure-spring-cloud-appconfiguration-config - azure-spring-cloud-appconfiguration-config-web - azure-spring-cloud-feature-management - azure-spring-cloud-feature-management-web - azure-spring-cloud-starter-appconfiguration-config + azure-data-appconfiguration + azure-data-appconfiguration-perf + azure-resourcemanager-appconfiguration diff --git a/sdk/appconfiguration/tests.yml b/sdk/appconfiguration/tests.yml index 085217d310b07..bfd9d5a61a5ee 100644 --- a/sdk/appconfiguration/tests.yml +++ b/sdk/appconfiguration/tests.yml @@ -4,16 +4,12 @@ stages: - template: /eng/pipelines/templates/stages/archetype-sdk-tests.yml parameters: ServiceDirectory: appconfiguration - Artifacts: - - name: azure-spring-cloud-test-appconfiguration-config - groupId: com.azure.spring - safeName: azurespringcloudtestappconfigurationconfig TimeoutInMinutes: 90 - SupportedClouds: 'Public,UsGov,China' + SupportedClouds: "Public,UsGov,China" EnvVars: AZURE_APPCONFIG_CONNECTION_STRING: $(AZURE_APPCONFIG_CONNECTION_STRING) AZURE_CLIENT_ID: $(aad-azure-sdk-test-client-id) AZURE_CLIENT_SECRET: $(aad-azure-sdk-test-client-secret) AZURE_TENANT_ID: $(aad-azure-sdk-test-tenant-id) - TestGoals: 'verify' - TestOptions: '-DskipSpringITs=false' + TestGoals: "verify" + TestOptions: "-DskipSpringITs=false" diff --git a/sdk/spring/ci.yml b/sdk/spring/ci.yml index 39daef15d9a7e..8cf2cb427ce18 100644 --- a/sdk/spring/ci.yml +++ b/sdk/spring/ci.yml @@ -203,6 +203,26 @@ parameters: displayName: 'spring-cloud-azure-starter-redis' type: boolean default: true +- name: release_springcloudazureappconfigurationconfig + displayName: 'spring-cloud-azure-appconfiguration-config' + type: boolean + default: true +- name: release_springcloudazureappconfigurationconfigweb + displayName: 'spring-cloud-azure-appconfiguration-config-web' + type: boolean + default: true +- name: release_springcloudazurefeaturemanagement + displayName: 'spring-cloud-azure-feature-management' + type: boolean + default: true +- name: release_springcloudazurefeaturemanagementweb + displayName: 'spring-cloud-azure-feature-management-web' + type: boolean + default: true +- name: release_springcloudazurestarterappconfigurationconfig + displayName: 'spring-cloud-azure-starter-appconfiguration-config' + type: boolean + default: true extends: template: ../../eng/pipelines/templates/stages/archetype-sdk-client.yml @@ -525,3 +545,23 @@ extends: skipUpdatePackageJson: true skipVerifyChangelog: true releaseInBatch: ${{ parameters.release_springcloudazurestarterredis }} + - name: spring-cloud-azure-appconfiguration-config + groupId: com.azure.spring + safeName: springcloudazureappconfigurationconfig + releaseInBatch: ${{ parameters.release_azurespringcloudappconfigurationconfig }} + - name: spring-cloud-azure-appconfiguration-config-web + groupId: com.azure.spring + safeName: springcloudazureappconfigurationconfigweb + releaseInBatch: ${{ parameters.release_azurespringcloudappconfigurationconfigweb }} + - name: spring-cloud-azure-feature-management + groupId: com.azure.spring + safeName: springcloudazurefeaturemanagement + releaseInBatch: ${{ parameters.release_azurespringcloudfeaturemanagement }} + - name: spring-cloud-azure-feature-management-web + groupId: com.azure.spring + safeName: springcloudazurefeaturemanagementweb + releaseInBatch: ${{ parameters.release_azurespringcloudfeaturemanagementweb }} + - name: spring-cloud-azure-starter-appconfiguration-config + groupId: com.azure.spring + safeName: springcloudazurestarterappconfigurationconfig + releaseInBatch: ${{ parameters.release_azurespringcloudstarterappconfigurationconfig }} diff --git a/sdk/spring/pom.xml b/sdk/spring/pom.xml index c9dc74ffd2904..a480a102467e4 100644 --- a/sdk/spring/pom.xml +++ b/sdk/spring/pom.xml @@ -1,6 +1,6 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 com.azure.spring spring-cloud-azure @@ -60,6 +60,12 @@ spring-cloud-azure-starter-jdbc-postgresql spring-cloud-azure-starter-redis spring-cloud-azure-integration-tests + spring-cloud-azure-test-appconfiguration-config + spring-cloud-azure-appconfiguration-config + spring-cloud-appconfiguration-config-web + spring-cloud-azure-feature-management + spring-cloud-azure-feature-management-web + spring-cloud-azure-starter-appconfiguration-config @@ -112,6 +118,12 @@ spring-cloud-azure-starter-jdbc-mysql spring-cloud-azure-starter-jdbc-postgresql spring-cloud-azure-starter-redis + spring-cloud-azure-test-appconfiguration-config + spring-cloud-azure-appconfiguration-config + spring-cloud-appconfiguration-config-web + spring-cloud-azure-feature-management + spring-cloud-azure-feature-management-web + spring-cloud-azure-starter-appconfiguration-config diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/CHANGELOG.md b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/CHANGELOG.md similarity index 99% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/CHANGELOG.md rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/CHANGELOG.md index f1228081b4844..158426f817799 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 2.12.0-beta.1 (Unreleased) +## 4.0.0-beta.1 (Unreleased) ### Features Added diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/README.md b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/README.md similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/README.md rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/README.md diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml new file mode 100644 index 0000000000000..45f7a2e6f87fc --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml @@ -0,0 +1,195 @@ + + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + 4.0.0 + + com.azure.spring + spring-cloud-azure-appconfiguration-config-web + 4.0.0-beta.1 + Azure Spring Cloud App Configuration Config Web + Integration of Spring Cloud Config and Azure App Configuration Service + + + false + + + + + + + com.azure.spring + spring-cloud-azure-appconfiguration-config + 4.0.0-beta.1 + + + org.springframework.boot + spring-boot-starter-web + 2.7.4 + + + org.springframework.boot + spring-boot-starter-actuator + 2.7.4 + true + + + org.springframework.cloud + spring-cloud-bus + 3.1.2 + true + + + org.junit.vintage + junit-vintage-engine + 5.8.2 + test + + + org.hamcrest + hamcrest-core + + + + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-core + 4.5.1 + test + + + org.springframework.boot + spring-boot-starter-test + 2.7.4 + test + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + org.springframework.boot:spring-boot-starter-actuator:[2.7.4] + org.springframework.boot:spring-boot-starter-web:[2.7.4] + org.springframework.cloud:spring-cloud-bus:[3.1.2] + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.2 + + + + com.azure.spring.cloud.starter.appconfiguration + + + true + + + + + + + + empty-javadoc-jar-with-readme + package + + jar + + + javadoc + ${project.basedir}/javadocTemp + + + + empty-source-jar-with-readme + package + + jar + + + sources + ${project.basedir}/sourceTemp + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.1 + + + attach-javadocs + + jar + + + true + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + copy-readme-to-javadocTemp-and-sourceTemp + prepare-package + + + Deleting existing ${project.basedir}/javadocTemp and + ${project.basedir}/sourceTemp + + + + + Copying ${project.basedir}/../README.md to + ${project.basedir}/javadocTemp/README.md + + + Copying ${project.basedir}/../README.md to + ${project.basedir}/sourceTemp/README.md + + + + + + run + + + + + + + + diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java similarity index 79% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java index 0e395cac0f2c6..5f58b3777f822 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java @@ -15,12 +15,12 @@ import org.springframework.context.annotation.Configuration; import com.azure.spring.cloud.config.AppConfigurationRefresh; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.web.pullrefresh.AppConfigurationEventListener; -import com.azure.spring.cloud.config.web.pushbusrefresh.AppConfigurationBusRefreshEndpoint; -import com.azure.spring.cloud.config.web.pushbusrefresh.AppConfigurationBusRefreshEventListener; -import com.azure.spring.cloud.config.web.pushrefresh.AppConfigurationRefreshEndpoint; -import com.azure.spring.cloud.config.web.pushrefresh.AppConfigurationRefreshEventListener; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.config.web.implementation.pullrefresh.AppConfigurationEventListener; +import com.azure.spring.cloud.config.web.implementation.pushbusrefresh.AppConfigurationBusRefreshEndpoint; +import com.azure.spring.cloud.config.web.implementation.pushbusrefresh.AppConfigurationBusRefreshEventListener; +import com.azure.spring.cloud.config.web.implementation.pushrefresh.AppConfigurationRefreshEndpoint; +import com.azure.spring.cloud.config.web.implementation.pushrefresh.AppConfigurationRefreshEventListener; /** * Sets up refresh methods based on dependencies. @@ -29,7 +29,7 @@ @EnableConfigurationProperties(AppConfigurationProperties.class) @RemoteApplicationEventScan @ConditionalOnBean(AppConfigurationRefresh.class) -public class AppConfigurationWebAutoConfiguration { +class AppConfigurationWebAutoConfiguration { /** * Listener for activity, to check for config store changes. @@ -39,7 +39,7 @@ public class AppConfigurationWebAutoConfiguration { */ @Bean @ConditionalOnClass(RefreshEndpoint.class) - public AppConfigurationEventListener configListener(AppConfigurationRefresh appConfigurationRefresh) { + AppConfigurationEventListener configListener(AppConfigurationRefresh appConfigurationRefresh) { return new AppConfigurationEventListener(appConfigurationRefresh); } @@ -51,7 +51,7 @@ public AppConfigurationEventListener configListener(AppConfigurationRefresh appC "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties", "org.springframework.cloud.endpoint.RefreshEndpoint" }) - public static class AppConfigurationPushRefreshConfiguration { + static class AppConfigurationPushRefreshConfiguration { /** * Creates Endpoint for push refresh. @@ -86,7 +86,7 @@ public AppConfigurationRefreshEventListener appConfigurationRefreshEventListener "org.springframework.cloud.bus.BusProperties", "org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent", "org.springframework.cloud.endpoint.RefreshEndpoint" }) - public static class AppConfigurationBusConfiguration { + static class AppConfigurationBusConfiguration { /** * Creates Endpoint for push bus refresh. @@ -97,7 +97,7 @@ public static class AppConfigurationBusConfiguration { * @return AppConfigurationBusRefreshEndpoint */ @Bean - public AppConfigurationBusRefreshEndpoint appConfigurationBusRefreshEndpoint(ApplicationContext context, + AppConfigurationBusRefreshEndpoint appConfigurationBusRefreshEndpoint(ApplicationContext context, BusProperties bus, AppConfigurationProperties appConfiguration, Destination.Factory destinationFactory) { return new AppConfigurationBusRefreshEndpoint(context, bus.getId(), destinationFactory, appConfiguration); } @@ -108,7 +108,7 @@ public AppConfigurationBusRefreshEndpoint appConfigurationBusRefreshEndpoint(App * @return AppConfigurationBusRefreshEventListener */ @Bean - public AppConfigurationBusRefreshEventListener appConfigurationBusRefreshEventListener( + AppConfigurationBusRefreshEventListener appConfigurationBusRefreshEventListener( AppConfigurationRefresh appConfigurationRefresh) { return new AppConfigurationBusRefreshEventListener(appConfigurationRefresh); } diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationEndpoint.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/AppConfigurationEndpoint.java similarity index 86% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationEndpoint.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/AppConfigurationEndpoint.java index d75f3f4316821..45603797c94b3 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationEndpoint.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/AppConfigurationEndpoint.java @@ -1,17 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.web; +package com.azure.spring.cloud.config.web.implementation; -import com.azure.spring.cloud.config.properties.ConfigStore; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreMonitoring.AccessToken; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreMonitoring.PushNotification; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import static com.azure.spring.cloud.config.web.AppConfigurationWebConstants.DATA; -import static com.azure.spring.cloud.config.web.AppConfigurationWebConstants.SYNC_TOKEN; -import static com.azure.spring.cloud.config.web.AppConfigurationWebConstants.VALIDATION_CODE_KEY; -import static com.azure.spring.cloud.config.web.AppConfigurationWebConstants.VALIDATION_TOPIC; +import static com.azure.spring.cloud.config.web.implementation.AppConfigurationWebConstants.DATA; +import static com.azure.spring.cloud.config.web.implementation.AppConfigurationWebConstants.SYNC_TOKEN; +import static com.azure.spring.cloud.config.web.implementation.AppConfigurationWebConstants.VALIDATION_CODE_KEY; +import static com.azure.spring.cloud.config.web.implementation.AppConfigurationWebConstants.VALIDATION_TOPIC; import java.io.IOException; import java.util.List; @@ -21,6 +15,12 @@ import javax.servlet.http.HttpServletRequest; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreMonitoring.AccessToken; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * Common class for authenticating refresh requests. */ diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebConstants.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/AppConfigurationWebConstants.java similarity index 95% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebConstants.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/AppConfigurationWebConstants.java index 4dc3bd98f8dc9..df66be56b1114 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebConstants.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/AppConfigurationWebConstants.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.web; +package com.azure.spring.cloud.config.web.implementation; /** * Constants used for validating refresh requests. diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pullrefresh/AppConfigurationEventListener.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pullrefresh/AppConfigurationEventListener.java similarity index 79% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pullrefresh/AppConfigurationEventListener.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pullrefresh/AppConfigurationEventListener.java index cdc6554dd493c..db6d0a3c830fd 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pullrefresh/AppConfigurationEventListener.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pullrefresh/AppConfigurationEventListener.java @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.web.pullrefresh; +package com.azure.spring.cloud.config.web.implementation.pullrefresh; -import static com.azure.spring.cloud.config.web.AppConfigurationWebConstants.ACTUATOR; -import static com.azure.spring.cloud.config.web.AppConfigurationWebConstants.APPCONFIGURATION_REFRESH; -import static com.azure.spring.cloud.config.web.AppConfigurationWebConstants.APPCONFIGURATION_REFRESH_BUS; +import static com.azure.spring.cloud.config.web.implementation.AppConfigurationWebConstants.ACTUATOR; +import static com.azure.spring.cloud.config.web.implementation.AppConfigurationWebConstants.APPCONFIGURATION_REFRESH; +import static com.azure.spring.cloud.config.web.implementation.AppConfigurationWebConstants.APPCONFIGURATION_REFRESH_BUS; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushbusrefresh/AppConfigurationBusRefreshEndpoint.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEndpoint.java similarity index 89% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushbusrefresh/AppConfigurationBusRefreshEndpoint.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEndpoint.java index 7ba79ae1bee77..a75fb7e13de30 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushbusrefresh/AppConfigurationBusRefreshEndpoint.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEndpoint.java @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.web.pushbusrefresh; +package com.azure.spring.cloud.config.web.implementation.pushbusrefresh; -import static com.azure.spring.cloud.config.web.AppConfigurationWebConstants.APPCONFIGURATION_REFRESH_BUS; -import static com.azure.spring.cloud.config.web.AppConfigurationWebConstants.VALIDATION_CODE_FORMAT_START; +import static com.azure.spring.cloud.config.web.implementation.AppConfigurationWebConstants.APPCONFIGURATION_REFRESH_BUS; +import static com.azure.spring.cloud.config.web.implementation.AppConfigurationWebConstants.VALIDATION_CODE_FORMAT_START; import java.io.IOException; import java.util.Map; @@ -23,8 +23,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.web.AppConfigurationEndpoint; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.config.web.implementation.AppConfigurationEndpoint; import com.fasterxml.jackson.databind.JsonNode; /** diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushbusrefresh/AppConfigurationBusRefreshEvent.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEvent.java similarity index 96% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushbusrefresh/AppConfigurationBusRefreshEvent.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEvent.java index f627d89c66a36..0c1cfdf4ecf93 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushbusrefresh/AppConfigurationBusRefreshEvent.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEvent.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.web.pushbusrefresh; +package com.azure.spring.cloud.config.web.implementation.pushbusrefresh; import org.springframework.cloud.bus.event.Destination; import org.springframework.cloud.bus.event.RemoteApplicationEvent; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushbusrefresh/AppConfigurationBusRefreshEventListener.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEventListener.java similarity index 95% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushbusrefresh/AppConfigurationBusRefreshEventListener.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEventListener.java index e37950be9e287..cdff1080fe93f 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushbusrefresh/AppConfigurationBusRefreshEventListener.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEventListener.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.web.pushbusrefresh; +package com.azure.spring.cloud.config.web.implementation.pushbusrefresh; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushrefresh/AppConfigurationRefreshEndpoint.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpoint.java similarity index 89% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushrefresh/AppConfigurationRefreshEndpoint.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpoint.java index ca324e08fb086..033cd154ceb87 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushrefresh/AppConfigurationRefreshEndpoint.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpoint.java @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.web.pushrefresh; +package com.azure.spring.cloud.config.web.implementation.pushrefresh; -import static com.azure.spring.cloud.config.web.AppConfigurationWebConstants.APPCONFIGURATION_REFRESH; -import static com.azure.spring.cloud.config.web.AppConfigurationWebConstants.VALIDATION_CODE_FORMAT_START; +import static com.azure.spring.cloud.config.web.implementation.AppConfigurationWebConstants.APPCONFIGURATION_REFRESH; +import static com.azure.spring.cloud.config.web.implementation.AppConfigurationWebConstants.VALIDATION_CODE_FORMAT_START; import java.io.IOException; import java.util.Map; @@ -22,8 +22,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.web.AppConfigurationEndpoint; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.config.web.implementation.AppConfigurationEndpoint; import com.fasterxml.jackson.databind.JsonNode; /** diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushrefresh/AppConfigurationRefreshEvent.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEvent.java similarity index 94% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushrefresh/AppConfigurationRefreshEvent.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEvent.java index c56a34de8b649..3c9e89c1e4a0c 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushrefresh/AppConfigurationRefreshEvent.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEvent.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.web.pushrefresh; +package com.azure.spring.cloud.config.web.implementation.pushrefresh; import org.springframework.context.ApplicationEvent; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushrefresh/AppConfigurationRefreshEventListener.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEventListener.java similarity index 95% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushrefresh/AppConfigurationRefreshEventListener.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEventListener.java index 5012b13b6b72d..0b8d3df4f2d06 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/pushrefresh/AppConfigurationRefreshEventListener.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEventListener.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.web.pushrefresh; +package com.azure.spring.cloud.config.web.implementation.pushrefresh; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java similarity index 62% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java index 9b9815428e3b7..efdf0c7f84347 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. /** - * Package contains classes for automatic refresh. + * Package contains classes for enabling auto refresh of Azure App Configuration stores */ package com.azure.spring.cloud.config.web; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/resources/META-INF/spring.factories b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/resources/META-INF/spring.factories similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/main/resources/META-INF/spring.factories rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/resources/META-INF/spring.factories diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfigurationTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfigurationTest.java similarity index 80% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfigurationTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfigurationTest.java index 896a32e752ab3..79ad7d523e1e4 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfigurationTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfigurationTest.java @@ -2,11 +2,11 @@ // Licensed under the MIT License. package com.azure.spring.cloud.config.web; -import static com.azure.spring.cloud.config.web.TestConstants.CONN_STRING_PROP; -import static com.azure.spring.cloud.config.web.TestConstants.STORE_ENDPOINT_PROP; -import static com.azure.spring.cloud.config.web.TestConstants.TEST_CONN_STRING; -import static com.azure.spring.cloud.config.web.TestConstants.TEST_STORE_NAME; -import static com.azure.spring.cloud.config.web.TestUtils.propPair; +import static com.azure.spring.cloud.config.web.implementation.TestConstants.CONN_STRING_PROP; +import static com.azure.spring.cloud.config.web.implementation.TestConstants.STORE_ENDPOINT_PROP; +import static com.azure.spring.cloud.config.web.implementation.TestConstants.TEST_CONN_STRING; +import static com.azure.spring.cloud.config.web.implementation.TestConstants.TEST_STORE_NAME; +import static com.azure.spring.cloud.config.web.implementation.TestUtils.propPair; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; @@ -20,8 +20,9 @@ import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent; import org.springframework.cloud.endpoint.RefreshEndpoint; -import com.azure.spring.cloud.config.AppConfigurationAutoConfiguration; -import com.azure.spring.cloud.config.AppConfigurationBootstrapConfiguration; +import com.azure.spring.cloud.autoconfigure.context.AzureGlobalPropertiesAutoConfiguration; +import com.azure.spring.cloud.config.implementation.config.AppConfigurationAutoConfiguration; +import com.azure.spring.cloud.config.implementation.config.AppConfigurationBootstrapConfiguration; public class AppConfigurationWebAutoConfigurationTest { @@ -30,7 +31,7 @@ public class AppConfigurationWebAutoConfigurationTest { propPair(STORE_ENDPOINT_PROP, TEST_STORE_NAME)) .withConfiguration(AutoConfigurations.of(AppConfigurationBootstrapConfiguration.class, AppConfigurationAutoConfiguration.class, AppConfigurationWebAutoConfiguration.class, - RefreshAutoConfiguration.class, PathDestinationFactory.class)) + RefreshAutoConfiguration.class, PathDestinationFactory.class, AzureGlobalPropertiesAutoConfiguration.class)) .withUserConfiguration(BusProperties.class); @Test @@ -80,8 +81,7 @@ public void pushRefresh() { @Test public void busRefresh() { CONTEXT_RUNNER - .run(context -> - assertThat(context) + .run(context -> assertThat(context) .hasBean("appConfigurationBusRefreshEndpoint")); } diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/AppConfigurationEndpointTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/AppConfigurationEndpointTest.java similarity index 98% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/AppConfigurationEndpointTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/AppConfigurationEndpointTest.java index 32b1e9ac61559..89a3d4c4bb241 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/AppConfigurationEndpointTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/AppConfigurationEndpointTest.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.web; +package com.azure.spring.cloud.config.web.implementation; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -25,7 +25,7 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import com.azure.spring.cloud.config.properties.ConfigStore; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/TestConstants.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/TestConstants.java similarity index 96% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/TestConstants.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/TestConstants.java index 020adacafeb5f..bbae24ae090b4 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/TestConstants.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/TestConstants.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.web; +package com.azure.spring.cloud.config.web.implementation; /** * Test constants which can be shared across different test classes diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/TestUtils.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/TestUtils.java similarity index 70% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/TestUtils.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/TestUtils.java index e1bde887540fe..40a0ba34a78dc 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/TestUtils.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/TestUtils.java @@ -1,15 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.web; - -import com.azure.data.appconfiguration.models.ConfigurationSetting; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreSelects; -import com.azure.spring.cloud.config.properties.ConfigStore; +package com.azure.spring.cloud.config.web.implementation; import java.util.ArrayList; import java.util.List; +import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationKeyValueSelector; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; + /** * Utility methods which can be used across different test classes */ @@ -18,7 +18,7 @@ public final class TestUtils { private TestUtils() { } - static String propPair(String propName, String propValue) { + public static String propPair(String propName, String propValue) { return String.format("%s=%s", propName, propValue); } @@ -42,8 +42,8 @@ static void addStore(AppConfigurationProperties properties, String storeEndpoint ConfigStore store = new ConfigStore(); store.setConnectionString(connectionString); store.setEndpoint(storeEndpoint); - AppConfigurationStoreSelects selectedKeys = new AppConfigurationStoreSelects().setKeyFilter("/application/").setLabelFilter(label); - List selects = new ArrayList<>(); + AppConfigurationKeyValueSelector selectedKeys = new AppConfigurationKeyValueSelector().setKeyFilter("/application/").setLabelFilter(label); + List selects = new ArrayList<>(); selects.add(selectedKeys); store.setSelects(selects); stores.add(store); diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/pullrefresh/AppConfigurationEventListenerTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/pullrefresh/AppConfigurationEventListenerTest.java similarity index 95% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/pullrefresh/AppConfigurationEventListenerTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/pullrefresh/AppConfigurationEventListenerTest.java index 357b7b57c53dd..0dc2b6a298b22 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/pullrefresh/AppConfigurationEventListenerTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/pullrefresh/AppConfigurationEventListenerTest.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.web.pullrefresh; +package com.azure.spring.cloud.config.web.implementation.pullrefresh; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/pushbusrefresh/AppConfigurationBusRefreshEndpointTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEndpointTest.java similarity index 91% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/pushbusrefresh/AppConfigurationBusRefreshEndpointTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEndpointTest.java index e67a0ee8745a1..bd3b1b95be386 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/pushbusrefresh/AppConfigurationBusRefreshEndpointTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEndpointTest.java @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.web.pushbusrefresh; +package com.azure.spring.cloud.config.web.implementation.pushbusrefresh; -import static com.azure.spring.cloud.config.web.TestConstants.TOPIC; -import static com.azure.spring.cloud.config.web.TestConstants.TRIGGER_KEY; -import static com.azure.spring.cloud.config.web.TestConstants.TRIGGER_LABEL; -import static com.azure.spring.cloud.config.web.TestConstants.VALIDATION_URL; +import static com.azure.spring.cloud.config.web.implementation.TestConstants.TOPIC; +import static com.azure.spring.cloud.config.web.implementation.TestConstants.TRIGGER_KEY; +import static com.azure.spring.cloud.config.web.implementation.TestConstants.TRIGGER_LABEL; +import static com.azure.spring.cloud.config.web.implementation.TestConstants.VALIDATION_URL; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; @@ -28,12 +28,12 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreMonitoring; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreMonitoring.AccessToken; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreMonitoring.PushNotification; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreTrigger; -import com.azure.spring.cloud.config.properties.ConfigStore; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreMonitoring; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreMonitoring.AccessToken; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreTrigger; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; public class AppConfigurationBusRefreshEndpointTest { diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/pushrefresh/AppConfigurationRefreshEndpointTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpointTest.java similarity index 91% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/pushrefresh/AppConfigurationRefreshEndpointTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpointTest.java index 71470fbf2cb40..a6b2ab32480eb 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/pushrefresh/AppConfigurationRefreshEndpointTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpointTest.java @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.web.pushrefresh; +package com.azure.spring.cloud.config.web.implementation.pushrefresh; -import static com.azure.spring.cloud.config.web.TestConstants.TOPIC; -import static com.azure.spring.cloud.config.web.TestConstants.TRIGGER_KEY; -import static com.azure.spring.cloud.config.web.TestConstants.TRIGGER_LABEL; -import static com.azure.spring.cloud.config.web.TestConstants.VALIDATION_URL; +import static com.azure.spring.cloud.config.web.implementation.TestConstants.TOPIC; +import static com.azure.spring.cloud.config.web.implementation.TestConstants.TRIGGER_KEY; +import static com.azure.spring.cloud.config.web.implementation.TestConstants.TRIGGER_LABEL; +import static com.azure.spring.cloud.config.web.implementation.TestConstants.VALIDATION_URL; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; @@ -29,12 +29,12 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreMonitoring; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreMonitoring.AccessToken; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreMonitoring.PushNotification; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreTrigger; -import com.azure.spring.cloud.config.properties.ConfigStore; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreMonitoring; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreMonitoring.AccessToken; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreTrigger; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; public class AppConfigurationRefreshEndpointTest { diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/resources/webHookInvalid.json b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/resources/webHookInvalid.json similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/resources/webHookInvalid.json rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/resources/webHookInvalid.json diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/resources/webHookRefresh.json b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/resources/webHookRefresh.json similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/resources/webHookRefresh.json rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/resources/webHookRefresh.json diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/resources/webHookValidation.json b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/resources/webHookValidation.json similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/src/test/resources/webHookValidation.json rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/resources/webHookValidation.json diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/CHANGELOG.md b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md similarity index 98% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/CHANGELOG.md rename to sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md index fd9c2efaad310..079a0556ed2ec 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 2.12.0-beta.1 (Unreleased) +## 4.0.0-beta.1 (Unreleased) ### Features Added @@ -63,7 +63,7 @@ Upgrade Spring Boot dependencies version to 2.7.7 and Spring Cloud dependencies This release is compatible with Spring Boot 2.5.0-2.5.11, 2.6.0-2.6.5. ### Features Added -- Added refresh interval parameter to `spring.cloud.azure.appconfiguraiton` to force refreshes on a given interval. Can be used to make sure secrets are kept up to date. +- Added refresh interval parameter to `spring.cloud.azure.appconfiguration` to force refreshes on a given interval. Can be used to make sure secrets are kept up to date. - Added BackoffTimeCalculator, which sets the next refresh period to sooner if a refresh fails. ### Dependency Upgrades diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/README.md b/sdk/spring/spring-cloud-azure-appconfiguration-config/README.md similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/README.md rename to sdk/spring/spring-cloud-azure-appconfiguration-config/README.md diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml new file mode 100644 index 0000000000000..84a7ddcf26aa1 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -0,0 +1,181 @@ + + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + 4.0.0 + + com.azure.spring + spring-cloud-azure-appconfiguration-config + 4.0.0-beta.1 + Azure Spring Cloud App Configuration Config + Integration of Spring Cloud Config and Azure App Configuration Service + + + false + + + + + + + org.springframework.boot + spring-boot-autoconfigure-processor + 2.7.8 + + + org.springframework.boot + spring-boot-autoconfigure + 2.7.8 + + + org.springframework.boot + spring-boot-configuration-processor + 2.7.8 + true + + + org.springframework.cloud + spring-cloud-starter-bootstrap + 3.1.5 + + + org.springframework.cloud + spring-cloud-context + 3.1.5 + + + com.fasterxml.jackson.core + jackson-annotations + 2.13.4 + + + com.fasterxml.jackson.core + jackson-databind + 2.13.4.2 + + + + com.azure + azure-core + 1.35.0 + + + com.azure + azure-data-appconfiguration + 1.4.1 + + + com.azure + azure-identity + 1.7.3 + + + com.azure + azure-security-keyvault-secrets + 4.5.3 + + + com.azure + azure-core-http-netty + 1.12.8 + + + org.hibernate.validator + hibernate-validator + 6.2.5.Final + + + org.springframework.boot + spring-boot-actuator-autoconfigure + 2.7.8 + + + com.azure.spring + spring-cloud-azure-service + 4.4.1 + + + com.azure.spring + spring-cloud-azure-service + 4.4.1 + + + com.azure.spring + spring-cloud-azure-actuator-autoconfigure + 4.4.1 + + + + + org.springframework.boot + spring-boot-starter-test + 2.7.8 + test + + + + + com.google.code.findbugs + jsr305 + 3.0.2 + provided + + + javax.annotation + javax.annotation-api + 1.3.2 + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.fasterxml.jackson.core:jackson-annotations:[2.13.4] + com.fasterxml.jackson.core:jackson-databind:[2.13.4.2] + javax.annotation:javax.annotation-api:[1.3.2] + org.apache.commons:commons-lang3:[3.12.0] + org.apache.httpcomponents:httpclient:[4.5.14] + org.hibernate.validator:hibernate-validator:[6.2.5.Final] + org.springframework.boot:spring-boot-autoconfigure-processor:[2.7.8] + org.springframework.boot:spring-boot-autoconfigure:[2.7.8] + org.springframework.boot:spring-boot-actuator-autoconfigure:[2.7.8] + org.springframework.boot:spring-boot-configuration-processor:[2.7.8] + org.springframework.cloud:spring-cloud-context:[3.1.5] + org.springframework.cloud:spring-cloud-starter-bootstrap:[3.1.5] + org.springframework:spring-web:[5.3.25] + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.2 + + + + true + true + + + + + + + \ No newline at end of file diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationRefresh.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationRefresh.java similarity index 94% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationRefresh.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationRefresh.java index c0beae5f02943..b44a372fc6638 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationRefresh.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationRefresh.java @@ -8,7 +8,7 @@ import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.scheduling.annotation.Async; -import com.azure.spring.cloud.config.health.AppConfigurationStoreHealth; +import com.azure.spring.cloud.config.implementation.health.AppConfigurationStoreHealth; /** * Enables checking of Configuration updates. diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/ConfigurationClientBuilderSetup.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/ConfigurationClientCustomizer.java similarity index 91% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/ConfigurationClientBuilderSetup.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/ConfigurationClientCustomizer.java index eb7d0e25486f9..50ef2137808b8 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/ConfigurationClientBuilderSetup.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/ConfigurationClientCustomizer.java @@ -8,7 +8,7 @@ /** * Creates Custom CustomClientBuilder for connecting to Azure App Configuration. */ -public interface ConfigurationClientBuilderSetup { +public interface ConfigurationClientCustomizer { /** * Updates the ConfigurationClientBuilder for connecting to the given App Configuration. diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/KeyVaultSecretProvider.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/KeyVaultSecretProvider.java similarity index 73% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/KeyVaultSecretProvider.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/KeyVaultSecretProvider.java index ecc202e4ad5c2..441a8aba2fe20 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/KeyVaultSecretProvider.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/KeyVaultSecretProvider.java @@ -8,10 +8,10 @@ public interface KeyVaultSecretProvider { /** - * Returns a secret value for a given uri - * @param uri Key Vault Reference + * Returns a secret value for a given endpoint + * @param endpoint Key Vault Reference * @return String value of the secret */ - String getSecret(String uri); + String getSecret(String endpoint); } diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/SecretClientBuilderSetup.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/SecretClientCustomizer.java similarity index 73% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/SecretClientBuilderSetup.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/SecretClientCustomizer.java index a320c9f4c3760..7c749b5f9df6c 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/SecretClientBuilderSetup.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/SecretClientCustomizer.java @@ -7,13 +7,13 @@ /** * Creates Custom SecretClientBuilder for connecting to Key Vault. */ -public interface SecretClientBuilderSetup { +public interface SecretClientCustomizer { /** - * Updates the SecretClientBuilder for connecting to the given uri. + * Updates the SecretClientBuilder for connecting to the given endpoint. * @param builder SecretClientBuilder - * @param uri String + * @param endpoint String */ - void setup(SecretClientBuilder builder, String uri); + void setup(SecretClientBuilder builder, String endpoint); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationApplicationSettingPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationApplicationSettingPropertySource.java new file mode 100644 index 0000000000000..5062275e15858 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationApplicationSettingPropertySource.java @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.azure.data.appconfiguration.models.SecretReferenceConfigurationSetting; +import com.azure.data.appconfiguration.models.SettingSelector; +import com.azure.security.keyvault.secrets.models.KeyVaultSecret; +import com.fasterxml.jackson.core.JsonProcessingException; + +/** + * Azure App Configuration PropertySource unique per Store Label(Profile) combo. + * + *

+ * i.e. If connecting to 2 stores and have 2 labels set 4 + * AppConfigurationPropertySources need to be created. + *

+ */ +final class AppConfigurationApplicationSettingPropertySource extends AppConfigurationPropertySource { + + private static final Logger LOGGER = LoggerFactory + .getLogger(AppConfigurationApplicationSettingPropertySource.class); + + private final AppConfigurationKeyVaultClientFactory keyVaultClientFactory; + + private final int maxRetryTime; + + AppConfigurationApplicationSettingPropertySource(String originEndpoint, AppConfigurationReplicaClient replicaClient, + AppConfigurationKeyVaultClientFactory keyVaultClientFactory, String keyFilter, String[] labelFilter, + int maxRetryTime) { + // The context alone does not uniquely define a PropertySource, append storeName + // and label to uniquely define a PropertySource + super(originEndpoint, replicaClient, keyFilter, labelFilter); + this.keyVaultClientFactory = keyVaultClientFactory; + this.maxRetryTime = maxRetryTime; + } + + /** + *

+ * Gets settings from Azure/Cache to set as configurations. Updates the cache. + *

+ * + * @throws JsonProcessingException thrown if fails to parse Json content type + */ + public void initProperties() throws JsonProcessingException { + List labels = Arrays.asList(labelFilter); + Collections.reverse(labels); + + for (String label : labels) { + SettingSelector settingSelector = new SettingSelector().setKeyFilter(keyFilter + "*") + .setLabelFilter(label); + + // * for wildcard match + List settings = replicaClient.listSettings(settingSelector); + + for (ConfigurationSetting setting : settings) { + String key = setting.getKey().trim().substring(keyFilter.length()) + .replace('/', '.'); + if (setting instanceof SecretReferenceConfigurationSetting) { + String entry = getKeyVaultEntry((SecretReferenceConfigurationSetting) setting); + + // Null in the case of failFast is false, will just skip entry. + if (entry != null) { + properties.put(key, entry); + } + } else if (StringUtils.hasText(setting.getContentType()) + && JsonConfigurationParser.isJsonContentType(setting.getContentType())) { + Map jsonSettings = JsonConfigurationParser.parseJsonSetting(setting); + for (Entry jsonSetting : jsonSettings.entrySet()) { + key = jsonSetting.getKey().trim().substring(keyFilter.length()); + properties.put(key, jsonSetting.getValue()); + } + } else { + properties.put(key, setting.getValue()); + } + } + } + } + + /** + * Given a Setting's Key Vault Reference stored in the Settings value, it will + * get its entry in Key Vault. + * + * @param secretReference {"uri": + * "<your-vault-url>/secret/<secret>/<version>"} + * @return Key Vault Secret Value + */ + private String getKeyVaultEntry(SecretReferenceConfigurationSetting secretReference) { + String secretValue = null; + try { + URI uri = null; + KeyVaultSecret secret = null; + + // Parsing Key Vault Reference for URI + try { + uri = new URI(secretReference.getSecretId()); + secret = keyVaultClientFactory.getClient("https://" + uri.getHost()).getSecret(uri, maxRetryTime); + } catch (URISyntaxException e) { + LOGGER.error("Error Processing Key Vault Entry URI."); + ReflectionUtils.rethrowRuntimeException(e); + } + + if (secret == null) { + throw new IOException("No Key Vault Secret found for Reference."); + } + secretValue = secret.getValue(); + } catch (RuntimeException | IOException e) { + LOGGER.error("Error Retrieving Key Vault Entry"); + ReflectionUtils.rethrowRuntimeException(e); + } + return secretValue; + } +} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationConstants.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationConstants.java similarity index 72% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationConstants.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationConstants.java index 085292af8ca8b..eb7f7e3d19f7e 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationConstants.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationConstants.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config; +package com.azure.spring.cloud.config.implementation; /** * Constants used for processing Azure App Configuration Config info. @@ -15,13 +15,12 @@ public class AppConfigurationConstants { /** * App Configurations Key Vault Reference Content Type */ - public static final String KEY_VAULT_CONTENT_TYPE = - "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"; + public static final String KEY_VAULT_CONTENT_TYPE = "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"; /** * Feature Management Key Prefix */ - public static final String FEATURE_MANAGEMENT_KEY = "feature-management.featureManagement"; + public static final String FEATURE_MANAGEMENT_KEY = "feature-management.featureManagement."; /** * Feature Flag Prefix @@ -42,17 +41,17 @@ public class AppConfigurationConstants { * Key for returning all feature flags */ public static final String FEATURE_STORE_WATCH_KEY = FEATURE_STORE_SUFFIX + "*"; - + /** * Constant for tracing if the library is being used with a dev profile. */ public static final String DEV_ENV_TRACING = "Dev"; - + /** * Constant for tracing if Key Vault is configured for use. */ public static final String KEY_VAULT_CONFIGURED_TRACING = "UsesKeyVault"; - + /** * Constant for tracing for Replica Count */ @@ -62,14 +61,30 @@ public class AppConfigurationConstants { * Http Header User Agent */ public static final String USER_AGENT_TYPE = "User-Agent"; - + /** * Http Header Correlation Context */ public static final String CORRELATION_CONTEXT = "Correlation-Context"; - + /** * Configuration Label for loading configurations with no label. */ public static final String EMPTY_LABEL = "\0"; + + public static final String USERS = "users"; + + public static final String USERS_CAPS = "Users"; + + public static final String AUDIENCE = "Audience"; + + public static final String GROUPS = "groups"; + + public static final String GROUPS_CAPS = "Groups"; + + public static final String TARGETING_FILTER = "targetingFilter"; + + public static final String DEFAULT_ROLLOUT_PERCENTAGE = "defaultRolloutPercentage"; + + public static final String DEFAULT_ROLLOUT_PERCENTAGE_CAPS = "DefaultRolloutPercentage"; } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySource.java new file mode 100644 index 0000000000000..04d475a52c90d --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySource.java @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation; + +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.AUDIENCE; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.DEFAULT_ROLLOUT_PERCENTAGE; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.DEFAULT_ROLLOUT_PERCENTAGE_CAPS; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.FEATURE_FLAG_PREFIX; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.FEATURE_MANAGEMENT_KEY; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.GROUPS; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.GROUPS_CAPS; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.TARGETING_FILTER; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.USERS; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.USERS_CAPS; +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toMap; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +import org.springframework.util.StringUtils; + +import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; +import com.azure.data.appconfiguration.models.FeatureFlagFilter; +import com.azure.data.appconfiguration.models.SettingSelector; +import com.azure.spring.cloud.config.implementation.feature.management.entity.Feature; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; + +/** + * Azure App Configuration PropertySource unique per Store Label(Profile) combo. + * + *

+ * i.e. If connecting to 2 stores and have 2 labels set 4 AppConfigurationPropertySources need to be created. + *

+ */ +final class AppConfigurationFeatureManagementPropertySource extends AppConfigurationPropertySource { + + private static final ObjectMapper CASE_INSENSITIVE_MAPPER = JsonMapper.builder() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build(); + + /** + * App Configuration Feature Filter prefix. + */ + private static final String KEY_FILTER_PREFIX = ".appconfig.featureflag/"; + + private static final String KEY_FILTER_DEFAULT = KEY_FILTER_PREFIX + "*"; + + private final List featureConfigurationSettings; + + AppConfigurationFeatureManagementPropertySource(String originEndpoint, AppConfigurationReplicaClient replicaClient, + String keyFilter, String[] labelFilter) { + super("FM_" + originEndpoint, replicaClient, keyFilter, labelFilter); + featureConfigurationSettings = new ArrayList<>(); + } + + private static List convertToListOrEmptyList(Map parameters, String key) { + List listObjects = CASE_INSENSITIVE_MAPPER.convertValue(parameters.get(key), + new TypeReference>() { + }); + return listObjects == null ? emptyList() : listObjects; + } + + /** + *

+ * Gets settings from Azure/Cache to set as configurations. Updates the cache. + *

+ * + *

+ * Note: Doesn't update Feature Management, just stores values in cache. Call {@code initFeatures} to update + * Feature Management, but make sure its done in the last {@code AppConfigurationPropertySource} + * AppConfigurationPropertySource} + *

+ * + */ + public void initProperties() { + SettingSelector settingSelector = new SettingSelector(); + + String keyFilter = KEY_FILTER_DEFAULT; + + if (StringUtils.hasText(this.keyFilter)) { + keyFilter = KEY_FILTER_PREFIX + this.keyFilter; + } + + settingSelector.setKeyFilter(keyFilter); + + List labels = Arrays.asList(labelFilter); + Collections.reverse(labels); + + for (String label : labels) { + settingSelector.setLabelFilter(label); + + List features = replicaClient.listSettings(settingSelector); + + // Reading In Features + for (ConfigurationSetting setting : features) { + if (setting instanceof FeatureFlagConfigurationSetting + && FEATURE_FLAG_CONTENT_TYPE.equals(setting.getContentType())) { + featureConfigurationSettings.add(setting); + Object feature = createFeature((FeatureFlagConfigurationSetting) setting); + + String configName = FEATURE_MANAGEMENT_KEY // TODO (mametcal) This is Wrong/Needs to be updated with Feature Management 4.0 + + setting.getKey().trim().substring(FEATURE_FLAG_PREFIX.length()); + + properties.put(configName, feature); + } + } + } + } + + List getFeatureFlagSettings() { + return featureConfigurationSettings; + } + + /** + * Creates a {@code Feature} from a {@code KeyValueItem} + * + * @param item Used to create Features before being converted to be set into properties. + * @return Feature created from KeyValueItem + */ + @SuppressWarnings("unchecked") + private Object createFeature(FeatureFlagConfigurationSetting item) { + String key = getFeatureSimpleName(item); + Feature feature = new Feature(key, item); + Map featureEnabledFor = feature.getEnabledFor(); + + // Setting Enabled For to null, but enabled = true will result in the feature + // being on. This is the case of a feature is on/off and set to on. This is to + // tell the difference between conditional/off which looks exactly the same... + // It should never be the case of Conditional On, and no filters coming from + // Azure, but it is a valid way from the config file, which should result in + // false being returned. + if (featureEnabledFor.size() == 0 && item.isEnabled()) { + return true; + } else if (!item.isEnabled()) { + return false; + } + for (int filter = 0; filter < feature.getEnabledFor().size(); filter++) { + FeatureFlagFilter featureFilterEvaluationContext = featureEnabledFor.get(filter); + Map parameters = featureFilterEvaluationContext.getParameters(); + + if (parameters == null || !TARGETING_FILTER.equals(featureEnabledFor.get(filter).getName())) { + continue; + } + + Object audienceObject = parameters.get(AUDIENCE); + if (audienceObject != null) { + parameters = (Map) audienceObject; + } + + List users = convertToListOrEmptyList(parameters, USERS_CAPS); + List groupRollouts = convertToListOrEmptyList(parameters, GROUPS_CAPS); + + switchKeyValues(parameters, USERS_CAPS, USERS, mapValuesByIndex(users)); + switchKeyValues(parameters, GROUPS_CAPS, GROUPS, mapValuesByIndex(groupRollouts)); + switchKeyValues(parameters, DEFAULT_ROLLOUT_PERCENTAGE_CAPS, DEFAULT_ROLLOUT_PERCENTAGE, + parameters.get(DEFAULT_ROLLOUT_PERCENTAGE_CAPS)); + + featureFilterEvaluationContext.setParameters(parameters); + featureEnabledFor.put(filter, featureFilterEvaluationContext); + feature.setEnabledFor(featureEnabledFor); + + } + return feature; + + } + + private String getFeatureSimpleName(ConfigurationSetting setting) { + return setting.getKey().trim().substring(FEATURE_FLAG_PREFIX.length()); + } + + private Map mapValuesByIndex(List users) { + return IntStream.range(0, users.size()).boxed().collect(toMap(String::valueOf, users::get)); + } + + private void switchKeyValues(Map parameters, String oldKey, String newKey, Object value) { + parameters.put(newKey, value); + parameters.remove(oldKey); + } +} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationKeyVaultClientFactory.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationKeyVaultClientFactory.java new file mode 100644 index 0000000000000..651ac270569b4 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationKeyVaultClientFactory.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation; + +import java.util.HashMap; +import java.util.Map; + +import com.azure.spring.cloud.config.KeyVaultSecretProvider; +import com.azure.spring.cloud.config.SecretClientCustomizer; +import com.azure.spring.cloud.config.implementation.stores.AppConfigurationSecretClientManager; +import com.azure.spring.cloud.service.implementation.keyvault.secrets.SecretClientBuilderFactory; + +public class AppConfigurationKeyVaultClientFactory { + + private final Map keyVaultClients; + + private final SecretClientCustomizer keyVaultClientProvider; + + private final KeyVaultSecretProvider keyVaultSecretProvider; + + private final SecretClientBuilderFactory secretClientFactory; + + private final boolean credentialsConfigured; + + private final boolean isConfigured; + + public AppConfigurationKeyVaultClientFactory(SecretClientCustomizer keyVaultClientProvider, + KeyVaultSecretProvider keyVaultSecretProvider, SecretClientBuilderFactory secretClientFactory, + boolean credentialsConfigured) { + this.keyVaultClientProvider = keyVaultClientProvider; + this.keyVaultSecretProvider = keyVaultSecretProvider; + this.secretClientFactory = secretClientFactory; + keyVaultClients = new HashMap<>(); + this.credentialsConfigured = credentialsConfigured; + isConfigured = keyVaultClientProvider != null || credentialsConfigured; + } + + public AppConfigurationSecretClientManager getClient(String host) { + // Check if we already have a client for this key vault, if not we will make + // one + if (!keyVaultClients.containsKey(host)) { + AppConfigurationSecretClientManager client = new AppConfigurationSecretClientManager(host, + keyVaultClientProvider, keyVaultSecretProvider, secretClientFactory, credentialsConfigured); + keyVaultClients.put(host, client); + } + return keyVaultClients.get(host); + } + + public boolean isConfigured() { + return isConfigured; + } +} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySource.java new file mode 100644 index 0000000000000..66b571caf04e0 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySource.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.env.EnumerablePropertySource; + +import com.azure.data.appconfiguration.ConfigurationClient; + +/** + * Azure App Configuration PropertySource unique per Store Label(Profile) combo. + * + *

+ * i.e. If connecting to 2 stores and have 2 labels set 4 AppConfigurationPropertySources need to be created. + *

+ */ +abstract class AppConfigurationPropertySource extends EnumerablePropertySource { + + protected final String keyFilter; + + protected final String[] labelFilter; + + protected final Map properties = new LinkedHashMap<>(); + + protected final AppConfigurationReplicaClient replicaClient; + + AppConfigurationPropertySource(String originEndpoint, AppConfigurationReplicaClient replicaClient, String keyFilter, + String[] labelFilter) { + // The context alone does not uniquely define a PropertySource, append storeName + // and label to uniquely define a PropertySource + super( + keyFilter + originEndpoint + "/" + getLabelName(labelFilter)); + this.replicaClient = replicaClient; + this.keyFilter = keyFilter; + this.labelFilter = labelFilter; + } + + @Override + public String[] getPropertyNames() { + Set keySet = properties.keySet(); + return keySet.toArray(new String[keySet.size()]); + } + + @Override + public Object getProperty(String name) { + return properties.get(name); + } + + private static String getLabelName(String[] labelFilter) { + StringBuilder labelName = new StringBuilder(); + for (String label : labelFilter) { + + labelName.append((labelName.length() == 0) ? label : "," + label); + } + return labelName.toString(); + } +} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocator.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocator.java similarity index 61% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocator.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocator.java index a035c3b62db5c..17b5297183a37 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocator.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocator.java @@ -4,11 +4,11 @@ import static org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration.BOOTSTRAP_PROPERTY_SOURCE_NAME; +import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -21,16 +21,12 @@ import org.springframework.core.env.PropertySource; import com.azure.data.appconfiguration.models.ConfigurationSetting; -import com.azure.data.appconfiguration.models.SettingSelector; -import com.azure.spring.cloud.config.KeyVaultCredentialProvider; -import com.azure.spring.cloud.config.KeyVaultSecretProvider; -import com.azure.spring.cloud.config.SecretClientBuilderSetup; -import com.azure.spring.cloud.config.feature.management.entity.FeatureSet; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationProviderProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreSelects; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreTrigger; -import com.azure.spring.cloud.config.properties.ConfigStore; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationKeyValueSelector; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProviderProperties; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreMonitoring; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreTrigger; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; +import com.azure.spring.cloud.config.implementation.properties.FeatureFlagKeyValueSelector; /** * Locates Azure App Configuration Property Sources. @@ -43,42 +39,34 @@ public final class AppConfigurationPropertySourceLocator implements PropertySour private static final String REFRESH_ARGS_PROPERTY_SOURCE = "refreshArgs"; - private final AppConfigurationProperties properties; - private final List configStores; private final AppConfigurationProviderProperties appProperties; private final AppConfigurationReplicaClientFactory clientFactory; - private final KeyVaultCredentialProvider keyVaultCredentialProvider; - - private final SecretClientBuilderSetup keyVaultClientProvider; + private final AppConfigurationKeyVaultClientFactory keyVaultClientFactory; - private final KeyVaultSecretProvider keyVaultSecretProvider; + private Duration refreshInterval; static final AtomicBoolean STARTUP = new AtomicBoolean(true); /** * Loads all Azure App Configuration Property Sources configured. + * * @param properties Configurations for stores to be loaded. * @param appProperties Configurations for the library. * @param clientFactory factory for creating clients for connecting to Azure App Configuration. - * @param keyVaultCredentialProvider optional provider for Key Vault Credentials - * @param keyVaultClientProvider optional provider for modifying the Key Vault Client - * @param keyVaultSecretProvider optional provider for loading secrets instead of connecting to Key Vault + * @param keyVaultClientFactory factory for creating clients for connecting to Azure Key Vault */ - public AppConfigurationPropertySourceLocator(AppConfigurationProperties properties, - AppConfigurationProviderProperties appProperties, AppConfigurationReplicaClientFactory clientFactory, - KeyVaultCredentialProvider keyVaultCredentialProvider, SecretClientBuilderSetup keyVaultClientProvider, - KeyVaultSecretProvider keyVaultSecretProvider) { - this.properties = properties; + public AppConfigurationPropertySourceLocator(AppConfigurationProviderProperties appProperties, + AppConfigurationReplicaClientFactory clientFactory, AppConfigurationKeyVaultClientFactory keyVaultClientFactory, + Duration refreshInterval, List configStores) { + this.refreshInterval = refreshInterval; this.appProperties = appProperties; - this.configStores = properties.getStores(); + this.configStores = configStores; this.clientFactory = clientFactory; - this.keyVaultCredentialProvider = keyVaultCredentialProvider; - this.keyVaultClientProvider = keyVaultClientProvider; - this.keyVaultSecretProvider = keyVaultSecretProvider; + this.keyVaultClientFactory = keyVaultClientFactory; BackoffTimeCalculator.setDefaults(appProperties.getDefaultMaxBackoff(), appProperties.getDefaultMinBackoff()); } @@ -92,7 +80,7 @@ public PropertySource locate(Environment environment) { ConfigurableEnvironment env = (ConfigurableEnvironment) environment; boolean currentlyLoaded = env.getPropertySources().stream().anyMatch(source -> { String storeName = configStores.get(0).getEndpoint(); - AppConfigurationStoreSelects selectedKey = configStores.get(0).getSelects().get(0); + AppConfigurationKeyValueSelector selectedKey = configStores.get(0).getSelects().get(0); return source.getName() .startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME + "-" + selectedKey.getKeyFilter() + storeName + "/"); }); @@ -106,18 +94,14 @@ public PropertySource locate(Environment environment) { Collections.reverse(configStores); // Last store has the highest precedence StateHolder newState = new StateHolder(); - newState.setNextForcedRefresh(properties.getRefreshInterval()); + newState.setNextForcedRefresh(refreshInterval); - Iterator configStoreIterator = configStores.iterator(); // Feature Management needs to be set in the last config store. - while (configStoreIterator.hasNext()) { - ConfigStore configStore = configStoreIterator.next(); - + for (ConfigStore configStore : configStores) { boolean loadNewPropertySources = STARTUP.get() || StateHolder.getLoadState(configStore.getEndpoint()); if (configStore.isEnabled() && loadNewPropertySources) { // There is only one Feature Set for all AppConfigurationPropertySources - FeatureSet featureSet = new FeatureSet(); List clients = clientFactory .getAvailableClients(configStore.getEndpoint(), true); @@ -132,40 +116,23 @@ public PropertySource locate(Environment environment) { if (!STARTUP.get() && reloadFailed && !AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(client, clientFactory, - configStore.getFeatureFlags())) { + configStore.getFeatureFlags(), profiles)) { // This store doesn't have any changes where to refresh store did. Skipping Checking next. continue; } - // Reverse in order to add Profile specific properties earlier, and last profile - // comes first + // Reverse in order to add Profile specific properties earlier, and last profile comes first try { - sourceList - .addAll(create(client, configStore, profiles, !configStoreIterator.hasNext(), featureSet)); + List sources = create(client, configStore, profiles); + sourceList.addAll(sources); LOGGER.debug("PropertySource context."); + setupMonitoring(configStore, client, sources, newState); - // Setting new ETag values for Watch - List watchKeysSettings = getWatchKeys(client, - configStore.getMonitoring().getTriggers()); - List watchKeysFeatures = getFeatureFlagWatchKeys(client, configStore, - newState); - - if (watchKeysFeatures.size() > 0) { - newState.setStateFeatureFlag(configStore.getEndpoint(), watchKeysFeatures, - configStore.getMonitoring().getFeatureFlagRefreshInterval()); - newState.setLoadStateFeatureFlag(configStore.getEndpoint(), true); - } else { - newState.setLoadStateFeatureFlag(configStore.getEndpoint(), false); - } - - newState.setState(configStore.getEndpoint(), watchKeysSettings, - configStore.getMonitoring().getRefreshInterval()); - newState.setLoadState(configStore.getEndpoint(), true); generatedPropertySources = true; } catch (AppConfigurationStatusException e) { reloadFailed = true; - clientFactory.backoffClient(configStore.getEndpoint(), client.getEndpoint()); + clientFactory.backoffClientClient(configStore.getEndpoint(), client.getEndpoint()); } catch (Exception e) { newState = failedToGeneratePropertySource(configStore, newState, e); @@ -200,6 +167,30 @@ public PropertySource locate(Environment environment) { return composite; } + private void setupMonitoring(ConfigStore configStore, AppConfigurationReplicaClient client, + List sources, StateHolder newState) { + AppConfigurationStoreMonitoring monitoring = configStore.getMonitoring(); + if (monitoring.isEnabled()) { + + // Setting new ETag values for Watch + List watchKeysSettings = getWatchKeys(client, + monitoring.getTriggers()); + List watchKeysFeatures = getFeatureFlagWatchKeys(configStore, + sources); + + if (watchKeysFeatures.size() > 0) { + newState.setStateFeatureFlag(configStore.getEndpoint(), watchKeysFeatures, + monitoring.getFeatureFlagRefreshInterval()); + } + + newState.setState(configStore.getEndpoint(), watchKeysSettings, + monitoring.getRefreshInterval()); + } + newState.setLoadState(configStore.getEndpoint(), true, configStore.isFailFast()); + newState.setLoadStateFeatureFlag(configStore.getEndpoint(), true, configStore.isFailFast()); + + } + private List getWatchKeys(AppConfigurationReplicaClient client, List triggers) { List watchKeysSettings = new ArrayList<>(); @@ -216,17 +207,17 @@ private List getWatchKeys(AppConfigurationReplicaClient cl return watchKeysSettings; } - private List getFeatureFlagWatchKeys(AppConfigurationReplicaClient client, - ConfigStore configStore, - StateHolder newState) { + private List getFeatureFlagWatchKeys(ConfigStore configStore, + List sources) { List watchKeysFeatures = new ArrayList<>(); if (configStore.getFeatureFlags().getEnabled()) { - SettingSelector settingSelector = new SettingSelector() - .setKeyFilter(configStore.getFeatureFlags().getKeyFilter()) - .setLabelFilter(configStore.getFeatureFlags().getLabelFilter()); - - watchKeysFeatures = client.listConfigurationSettings(settingSelector); + for (AppConfigurationPropertySource propertySource : sources) { + if (propertySource instanceof AppConfigurationFeatureManagementPropertySource) { + watchKeysFeatures = ((AppConfigurationFeatureManagementPropertySource) propertySource) + .getFeatureFlagSettings(); + } + } } return watchKeysFeatures; } @@ -235,27 +226,23 @@ private StateHolder failedToGeneratePropertySource(ConfigStore configStore, Stat String message = "Failed to generate property sources for " + configStore.getEndpoint(); if (!STARTUP.get()) { // Need to check for refresh first, or reset will never happen if fail fast is true. - LOGGER.error( - "Refreshing failed while reading configuration from Azure App Configuration store " - + configStore.getEndpoint() + "."); + LOGGER.error("Refreshing failed while reading configuration from Azure App Configuration store " + + configStore.getEndpoint() + "."); - if (properties.getRefreshInterval() != null) { + if (refreshInterval != null) { // The next refresh will happen sooner if refresh interval is expired. - newState.updateNextRefreshTime(properties.getRefreshInterval(), appProperties.getDefaultMinBackoff()); + newState.updateNextRefreshTime(refreshInterval, appProperties.getDefaultMinBackoff()); } throw new RuntimeException(message, e); } else if (configStore.isFailFast()) { - LOGGER.error( - "Fail fast is set and there was an error reading configuration from Azure App " - + "Configuration store " + configStore.getEndpoint() + "."); + LOGGER.error("Fail fast is set and there was an error reading configuration from Azure App " + + "Configuration store " + configStore.getEndpoint() + "."); delayException(); throw new RuntimeException(message, e); } else { LOGGER.warn( - "Unable to load configuration from Azure AppConfiguration store " - + configStore.getEndpoint() + ".", - e); - newState.setLoadState(configStore.getEndpoint(), false); + "Unable to load configuration from Azure AppConfiguration store " + configStore.getEndpoint() + ".", e); + newState.setLoadState(configStore.getEndpoint(), false, configStore.isFailFast()); } return newState; } @@ -265,26 +252,32 @@ private StateHolder failedToGeneratePropertySource(ConfigStore configStore, Stat * * @param client client for connecting to App Configuration * @param store Config Store the PropertySource is being generated from - * @param initFeatures determines if Feature Management is set in the PropertySource. When generating more than one * @param profiles active profiles to be used as labels. it needs to be in the last one. * @return a list of AppConfigurationPropertySources + * @throws Exception creating a property source failed */ private List create(AppConfigurationReplicaClient client, ConfigStore store, - List profiles, boolean initFeatures, FeatureSet featureSet) throws Exception { + List profiles) throws Exception { List sourceList = new ArrayList<>(); + List selects = store.getSelects(); - List selects = store.getSelects(); + for (AppConfigurationKeyValueSelector selectedKeys : selects) { + AppConfigurationApplicationSettingPropertySource propertySource = new AppConfigurationApplicationSettingPropertySource( + store.getEndpoint(), client, keyVaultClientFactory, selectedKeys.getKeyFilter(), + selectedKeys.getLabelFilter(profiles), appProperties.getMaxRetryTime()); + propertySource.initProperties(); + sourceList.add(propertySource); + } - for (AppConfigurationStoreSelects selectedKeys : selects) { - AppConfigurationPropertySource propertySource = new AppConfigurationPropertySource(store, selectedKeys, - profiles, properties, client, appProperties, keyVaultCredentialProvider, - keyVaultClientProvider, keyVaultSecretProvider); + if (store.getFeatureFlags().getEnabled()) { + for (FeatureFlagKeyValueSelector selectedKeys : store.getFeatureFlags().getSelects()) { + AppConfigurationFeatureManagementPropertySource propertySource = new AppConfigurationFeatureManagementPropertySource( + store.getEndpoint(), client, selectedKeys.getKeyFilter(), + selectedKeys.getLabelFilter(profiles)); - propertySource.initProperties(featureSet); - if (initFeatures) { - propertySource.initFeatures(featureSet); + propertySource.initProperties(); + sourceList.add(propertySource); } - sourceList.add(propertySource); } return sourceList; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefresh.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefresh.java similarity index 83% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefresh.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefresh.java index 0fe6fe9d65e5f..7b0379b35fecb 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefresh.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefresh.java @@ -3,6 +3,8 @@ package com.azure.spring.cloud.config.implementation; import java.time.Duration; +import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; @@ -11,20 +13,22 @@ import org.slf4j.LoggerFactory; import org.springframework.cloud.endpoint.event.RefreshEvent; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.stereotype.Component; import com.azure.spring.cloud.config.AppConfigurationRefresh; -import com.azure.spring.cloud.config.health.AppConfigurationStoreHealth; import com.azure.spring.cloud.config.implementation.AppConfigurationRefreshUtil.RefreshEventData; -import com.azure.spring.cloud.config.pipline.policies.BaseAppConfigurationPolicy; +import com.azure.spring.cloud.config.implementation.health.AppConfigurationStoreHealth; +import com.azure.spring.cloud.config.implementation.pipline.policies.BaseAppConfigurationPolicy; /** * Enables checking of Configuration updates. */ @Component -public class AppConfigurationPullRefresh implements AppConfigurationRefresh { +public class AppConfigurationPullRefresh implements AppConfigurationRefresh, EnvironmentAware { private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationPullRefresh.class); @@ -38,12 +42,14 @@ public class AppConfigurationPullRefresh implements AppConfigurationRefresh { private final Duration refreshInterval; + private List profiles; + /** * Component used for checking for and triggering configuration refreshes. * - * @param appProperties Library properties for configuring backoff - * @param clientFactory Clients stores used to connect to App Configuration. - * @param defaultMinBackoff default minimum backoff time + * @param clientFactory Clients stores used to connect to App Configuration. * @param defaultMinBackoff default + * @param refreshInterval time between refresh intervals + * @param defaultMinBackoff minimum time between backoff retries minimum backoff time */ public AppConfigurationPullRefresh(AppConfigurationReplicaClientFactory clientFactory, Duration refreshInterval, Long defaultMinBackoff) { @@ -96,17 +102,15 @@ private boolean refreshStores() { if (running.compareAndSet(false, true)) { BaseAppConfigurationPolicy.setWatchRequests(true); try { - RefreshEventData eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactory, - refreshInterval, defaultMinBackoff); + refreshInterval, profiles, defaultMinBackoff); if (eventData.getDoRefresh()) { publisher.publishEvent(new RefreshEvent(this, eventData, eventData.getMessage())); return true; } } catch (Exception e) { // The next refresh will happen sooner if refresh interval is expired. - StateHolder.getCurrentState().updateNextRefreshTime(refreshInterval, - defaultMinBackoff); + StateHolder.getCurrentState().updateNextRefreshTime(refreshInterval, defaultMinBackoff); throw e; } finally { running.set(false); @@ -120,4 +124,9 @@ public Map getAppConfigurationStoresHealth( return clientFactory.getHealth(); } + @Override + public void setEnvironment(Environment environment) { + profiles = Arrays.asList(environment.getActiveProfiles()); + } + } diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtil.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtil.java similarity index 68% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtil.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtil.java index f35594d5e6efd..8776212b5463e 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtil.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtil.java @@ -9,12 +9,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; -import com.azure.spring.cloud.config.pipline.policies.BaseAppConfigurationPolicy; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreMonitoring; -import com.azure.spring.cloud.config.properties.FeatureFlagStore; +import com.azure.spring.cloud.config.implementation.pipline.policies.BaseAppConfigurationPolicy; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreMonitoring; +import com.azure.spring.cloud.config.implementation.properties.FeatureFlagKeyValueSelector; +import com.azure.spring.cloud.config.implementation.properties.FeatureFlagStore; + +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.FEATURE_FLAG_PREFIX; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.FEATURE_STORE_WATCH_KEY; class AppConfigurationRefreshUtil { @@ -27,7 +32,7 @@ class AppConfigurationRefreshUtil { * @return If a refresh event is called. */ static RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientFactory, - Duration refreshInterval, Long defaultMinBackoff) { + Duration refreshInterval, List profiles, Long defaultMinBackoff) { RefreshEventData eventData = new RefreshEventData(); BaseAppConfigurationPolicy.setWatchRequests(true); @@ -38,14 +43,16 @@ static RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory LOGGER.info(eventDataInfo); - eventData.setMessage(eventDataInfo); + eventData.setFullMessage(eventDataInfo); + return eventData; } - for (Entry entry : clientFactory.getConnections().entrySet()) { + for (Entry entry : clientFactory.getConnections().entrySet()) { String originEndpoint = entry.getKey(); ConnectionManager connection = entry.getValue(); // For safety reset current used replica. clientFactory.setCurrentConfigStoreClient(originEndpoint, originEndpoint); + AppConfigurationStoreMonitoring monitor = connection.getMonitoring(); List clients = clientFactory.getAvailableClients(originEndpoint); @@ -65,10 +72,8 @@ static RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory LOGGER.warn("Failed attempting to connect to " + client.getEndpoint() + " during refresh check."); - clientFactory.backoffClient(originEndpoint, client.getEndpoint()); - continue; + clientFactory.backoffClientClient(originEndpoint, client.getEndpoint()); } - } } else { LOGGER.debug("Skipping configuration refresh check for " + originEndpoint); @@ -82,20 +87,18 @@ static RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory refreshWithTimeFeatureFlags(client, featureStore, StateHolder.getStateFeatureFlag(originEndpoint), monitor.getFeatureFlagRefreshInterval(), - eventData); + eventData, profiles); if (eventData.getDoRefresh()) { - clientFactory.setCurrentConfigStoreClient(originEndpoint, - client.getEndpoint()); + clientFactory.setCurrentConfigStoreClient(originEndpoint, client.getEndpoint()); return eventData; } // If check didn't throw an error other clients don't need to be checked. break; } catch (AppConfigurationStatusException e) { LOGGER.warn("Failed attempting to connect to " + client.getEndpoint() - + " durring refresh check."); + + " during refresh check."); - clientFactory.backoffClient(originEndpoint, client.getEndpoint()); - continue; + clientFactory.backoffClientClient(originEndpoint, client.getEndpoint()); } } } else { @@ -112,16 +115,17 @@ static RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory } static boolean checkStoreAfterRefreshFailed(AppConfigurationReplicaClient client, - AppConfigurationReplicaClientFactory clientFactory, FeatureFlagStore featureStore) { + AppConfigurationReplicaClientFactory clientFactory, FeatureFlagStore featureStore, List profiles) { return refreshStoreCheck(client, clientFactory.findOriginForEndpoint(client.getEndpoint())) - || refreshStoreFeatureFlagCheck(featureStore, client); + || refreshStoreFeatureFlagCheck(featureStore, client, profiles); } /** * This is for a refresh fail only. - * @param client - * @param originEndpoint - * @return + * + * @param client Client checking for refresh + * @param originEndpoint config store origin endpoint + * @return A refresh should be triggered. */ private static boolean refreshStoreCheck(AppConfigurationReplicaClient client, String originEndpoint) { RefreshEventData eventData = new RefreshEventData(); @@ -133,18 +137,19 @@ private static boolean refreshStoreCheck(AppConfigurationReplicaClient client, S /** * This is for a refresh fail only. - * @param featureStore - * @param client - * @return + * @param featureStore Feature info for the store + * @param profiles Current configured profiles, can be used as labels. + * @param client Client checking for refresh + * @return true if a refresh should be triggered. */ private static boolean refreshStoreFeatureFlagCheck(FeatureFlagStore featureStore, - AppConfigurationReplicaClient client) { + AppConfigurationReplicaClient client, List profiles) { RefreshEventData eventData = new RefreshEventData(); String endpoint = client.getEndpoint(); if (featureStore.getEnabled() && StateHolder.getLoadStateFeatureFlag(endpoint)) { refreshWithoutTimeFeatureFlags(client, featureStore, - StateHolder.getStateFeatureFlag(endpoint).getWatchKeys(), eventData); + StateHolder.getStateFeatureFlag(endpoint).getWatchKeys(), eventData, profiles); } else { LOGGER.debug("Skipping feature flag refresh check for " + endpoint); } @@ -156,6 +161,7 @@ private static boolean refreshStoreFeatureFlagCheck(FeatureFlagStore featureStor * * @param state The refresh state of the endpoint being checked. * @param refreshInterval Amount of time to wait until next check of this endpoint. + * @param eventData Info for this refresh event. */ private static void refreshWithTime(AppConfigurationReplicaClient client, State state, Duration refreshInterval, RefreshEventData eventData) throws AppConfigurationStatusException { @@ -164,8 +170,9 @@ private static void refreshWithTime(AppConfigurationReplicaClient client, State refreshWithoutTime(client, state.getWatchKeys(), eventData); if (eventData.getDoRefresh()) { - // Just need to reset refreshInterval, if a refresh was triggered it will br updated after loading the - // new configurations. + // Just need to reset refreshInterval, if a refresh was triggered it will be updated after loading the + // new + // configurations. StateHolder.getCurrentState().updateStateRefresh(state, refreshInterval); } } @@ -174,9 +181,9 @@ private static void refreshWithTime(AppConfigurationReplicaClient client, State /** * Checks refresh trigger for etag changes. If they have changed a RefreshEventData is published. * - * @param client Replica client checking for refresh - * @param watchKeys watch keys checked for refresh - * @param eventData This refresh event + * @param client Client checking for refresh + * @param watchKeys Watch keys for the store. + * @param eventData Refresh event info */ private static void refreshWithoutTime(AppConfigurationReplicaClient client, List watchKeys, RefreshEventData eventData) throws AppConfigurationStatusException { @@ -195,43 +202,52 @@ private static void refreshWithoutTime(AppConfigurationReplicaClient client, } private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient client, - FeatureFlagStore featureStore, State state, Duration refreshInterval, RefreshEventData eventData) - throws AppConfigurationStatusException { + FeatureFlagStore featureStore, State state, Duration refreshInterval, RefreshEventData eventData, + List profiles) throws AppConfigurationStatusException { Instant date = Instant.now(); if (date.isAfter(state.getNextRefreshCheck())) { - SettingSelector selector = new SettingSelector().setKeyFilter(featureStore.getKeyFilter()) - .setLabelFilter(featureStore.getLabelFilter()); - List currentKeys = client.listConfigurationSettings(selector); - int watchedKeySize = 0; + for (FeatureFlagKeyValueSelector watchKey : featureStore.getSelects()) { + String keyFilter = FEATURE_STORE_WATCH_KEY; - keyCheck: for (ConfigurationSetting currentKey : currentKeys) { + if (StringUtils.hasText(watchKey.getKeyFilter())) { + keyFilter = FEATURE_FLAG_PREFIX + watchKey.getKeyFilter(); + } - watchedKeySize += 1; - for (ConfigurationSetting watchFlag : state.getWatchKeys()) { + SettingSelector selector = new SettingSelector().setKeyFilter(keyFilter) + .setLabelFilter(watchKey.getLabelFilterText(profiles)); + List currentKeys = client.listSettings(selector); - // If there is no result, etag will be considered empty. - // A refresh will trigger once the selector returns a value. - if (watchFlag != null && watchFlag.getKey().equals(currentKey.getKey()) - && watchFlag.getLabel().equals(currentKey.getLabel())) { - checkETag(watchFlag, currentKey, client.getEndpoint(), eventData); - if (eventData.getDoRefresh()) { - break keyCheck; + int watchedKeySize = 0; + + keyCheck: for (ConfigurationSetting currentKey : currentKeys) { + watchedKeySize += 1; + for (ConfigurationSetting watchFlag : state.getWatchKeys()) { + + // If there is no result, etag will be considered empty. + // A refresh will trigger once the selector returns a value. + if (watchFlag != null && watchFlag.getKey().equals(currentKey.getKey()) + && watchFlag.getLabel().equals(currentKey.getLabel())) { + checkETag(watchFlag, currentKey, client.getEndpoint(), eventData); + if (eventData.getDoRefresh()) { + break keyCheck; + + } } - } + } } - } - if (watchedKeySize != state.getWatchKeys().size()) { - String eventDataInfo = ".appconfig.featureflag/*"; + if (watchedKeySize != state.getWatchKeys().size()) { + String eventDataInfo = ".appconfig.featureflag/*"; - // Only one refresh Event needs to be call to update all of the - // stores, not one for each. - LOGGER.info("Configuration Refresh Event triggered by " + eventDataInfo); + // Only one refresh Event needs to be call to update all of the + // stores, not one for each. + LOGGER.info("Configuration Refresh Event triggered by " + eventDataInfo); - eventData.setMessage(eventDataInfo); + eventData.setMessage(eventDataInfo); + } } // Just need to reset refreshInterval, if a refresh was triggered it will be updated after loading the new @@ -241,41 +257,43 @@ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient cl } private static void refreshWithoutTimeFeatureFlags(AppConfigurationReplicaClient client, - FeatureFlagStore featureStore, List watchKeys, RefreshEventData eventData) - throws AppConfigurationStatusException { - SettingSelector selector = new SettingSelector().setKeyFilter(featureStore.getKeyFilter()) - .setLabelFilter(featureStore.getLabelFilter()); - List currentTriggerConfigurations = client.listConfigurationSettings(selector); - - int watchedKeySize = 0; - - for (ConfigurationSetting currentTriggerConfiguration : currentTriggerConfigurations) { - watchedKeySize += 1; - for (ConfigurationSetting watchFlag : watchKeys) { - - // If there is no result, etag will be considered empty. - // A refresh will trigger once the selector returns a value. - if (watchFlag != null && watchFlag.getKey().equals(currentTriggerConfiguration.getKey()) - && watchFlag.getLabel().equals(currentTriggerConfiguration.getLabel())) { - checkETag(watchFlag, currentTriggerConfiguration, client.getEndpoint(), eventData); - if (eventData.getDoRefresh()) { - return; + FeatureFlagStore featureStore, List watchKeys, RefreshEventData eventData, + List profiles) throws AppConfigurationStatusException { + for (FeatureFlagKeyValueSelector watchKey : featureStore.getSelects()) { + SettingSelector selector = new SettingSelector().setKeyFilter(watchKey.getKeyFilter()) + .setLabelFilter(watchKey.getLabelFilterText(profiles)); + List currentTriggerConfigurations = client.listSettings(selector); + + int watchedKeySize = 0; + + for (ConfigurationSetting currentTriggerConfiguration : currentTriggerConfigurations) { + watchedKeySize += 1; + for (ConfigurationSetting watchFlag : watchKeys) { + + // If there is no result, etag will be considered empty. + // A refresh will trigger once the selector returns a value. + if (watchFlag != null && watchFlag.getKey().equals(currentTriggerConfiguration.getKey()) + && watchFlag.getLabel().equals(currentTriggerConfiguration.getLabel())) { + checkETag(watchFlag, currentTriggerConfiguration, client.getEndpoint(), eventData); + if (eventData.getDoRefresh()) { + return; + } + } else { + break; } - } else { - break; - } + } } - } - if (watchedKeySize != watchKeys.size()) { - String eventDataInfo = ".appconfig.featureflag/*"; + if (watchedKeySize != watchKeys.size()) { + String eventDataInfo = ".appconfig.featureflag/*"; - // Only one refresh Event needs to be call to update all of the - // stores, not one for each. - LOGGER.info("Configuration Refresh Event triggered by " + eventDataInfo); + // Only one refresh Event needs to be call to update all of the + // stores, not one for each. + LOGGER.info("Configuration Refresh Event triggered by " + eventDataInfo); - eventData.setMessage(eventDataInfo); + eventData.setMessage(eventDataInfo); + } } } @@ -317,7 +335,12 @@ static class RefreshEventData { } RefreshEventData setMessage(String prefix) { - this.message = String.format(MSG_TEMPLATE, prefix); + setFullMessage(String.format(MSG_TEMPLATE, prefix)); + return this; + } + + RefreshEventData setFullMessage(String message) { + this.message = message; this.doRefresh = true; return this; } diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClient.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClient.java similarity index 92% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClient.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClient.java index 3f3235459ad6a..acb0e0a82ea58 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClient.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClient.java @@ -6,12 +6,13 @@ import java.util.ArrayList; import java.util.List; +import org.springframework.util.StringUtils; + import com.azure.core.exception.HttpResponseException; import com.azure.core.http.rest.PagedIterable; import com.azure.data.appconfiguration.ConfigurationClient; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; -import com.azure.spring.cloud.config.NormalizeNull; /** * Client for connecting to App Configuration when multiple replicas are in use. @@ -76,7 +77,8 @@ String getEndpoint() { * @param label String value of the watch key, use \0 for null. * @return The first returned configuration. */ - ConfigurationSetting getWatchKey(String key, String label) throws HttpResponseException { + ConfigurationSetting getWatchKey(String key, String label) + throws HttpResponseException { try { ConfigurationSetting watchKey = NormalizeNull .normalizeNullLabel(client.getConfigurationSetting(key, label)); @@ -89,7 +91,7 @@ ConfigurationSetting getWatchKey(String key, String label) throws HttpResponseEx throw new AppConfigurationStatusException(e.getMessage(), e.getResponse(), e.getValue()); } throw e; - } catch (Exception e) { // TODO (mametcal) This should be an UnkownHostException, but currently it isn't + } catch (Exception e) { // TODO (mametcal) This should be an UnknownHostException, but currently it isn't // catchable. if (e.getMessage().startsWith("java.net.UnknownHostException") || e.getMessage().startsWith("java.net.WebSocketHandshakeException") @@ -100,7 +102,6 @@ ConfigurationSetting getWatchKey(String key, String label) throws HttpResponseEx } throw e; } - } /** @@ -109,13 +110,13 @@ ConfigurationSetting getWatchKey(String key, String label) throws HttpResponseEx * @param settingSelector Information on which setting to pull. i.e. number of results, key value... * @return List of Configuration Settings. */ - List listConfigurationSettings(SettingSelector settingSelector) + List listSettings(SettingSelector settingSelector) throws HttpResponseException { List configurationSettings = new ArrayList<>(); try { PagedIterable settings = client.listConfigurationSettings(settingSelector); - settings.forEach(setting -> configurationSettings.add(NormalizeNull.normalizeNullLabel(setting))); this.failedAttempts = 0; + settings.forEach(setting -> configurationSettings.add(NormalizeNull.normalizeNullLabel(setting))); return configurationSettings; } catch (HttpResponseException e) { int statusCode = e.getResponse().getStatusCode(); @@ -124,8 +125,8 @@ List listConfigurationSettings(SettingSelector settingSele throw new AppConfigurationStatusException(e.getMessage(), e.getResponse(), e.getValue()); } throw e; - } catch (Exception e) { // TODO (mametcal) This should be an UnkownHostException, but currently it isn't - // catchable. + } catch (Exception e) { // TODO (mametcal) This should be an UnknownHostException, but currently it isn't + // catchable. if (e.getMessage().startsWith("java.net.UnknownHostException") || e.getMessage().startsWith("java.net.WebSocketHandshakeException") || e.getMessage().startsWith("java.net.SocketException") @@ -142,7 +143,7 @@ List listConfigurationSettings(SettingSelector settingSele * @param syncToken the sync token. */ void updateSyncToken(String syncToken) { - if (syncToken != null) { + if (StringUtils.hasText(syncToken)) { client.updateSyncToken(syncToken); } } diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactory.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactory.java similarity index 88% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactory.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactory.java index 06c8172c1458f..bf2a47f38343f 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactory.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactory.java @@ -6,9 +6,8 @@ import java.util.List; import java.util.Map; -import com.azure.spring.cloud.config.health.AppConfigurationStoreHealth; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.properties.ConfigStore; +import com.azure.spring.cloud.config.implementation.health.AppConfigurationStoreHealth; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; /** * Manages all client connections for all configuration stores. @@ -22,13 +21,14 @@ public class AppConfigurationReplicaClientFactory { /** * Sets up Connections to all configuration stores. * - * @param properties client properties + * @param clientBuilder builder for app configuration replica clients + * @param configStores configuration info for config stores */ public AppConfigurationReplicaClientFactory(AppConfigurationReplicaClientsBuilder clientBuilder, - AppConfigurationProperties properties) { - this.configStores = properties.getStores(); + List configStores) { + this.configStores = configStores; if (CONNECTIONS.size() == 0) { - for (ConfigStore store : properties.getStores()) { + for (ConfigStore store : configStores) { ConnectionManager manager = new ConnectionManager(clientBuilder, store); CONNECTIONS.put(manager.getOriginEndpoint(), manager); } @@ -45,7 +45,7 @@ public Map getConnections() { /** * @return the configStores */ - List getConfigStores() { + List getConfigStores() { // TODO (mametcal) This is never used? return configStores; } @@ -72,7 +72,7 @@ List getAvailableClients(String originEndpoint, B * @param originEndpoint identifier of the store. The identifier is the primary endpoint of the store. * @param endpoint replica endpoint */ - void backoffClient(String originEndpoint, String endpoint) { + void backoffClientClient(String originEndpoint, String endpoint) { CONNECTIONS.get(originEndpoint).backoffClient(endpoint); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java new file mode 100644 index 0000000000000..8b788a85d3287 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java @@ -0,0 +1,255 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.convert.DurationStyle; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.azure.core.http.policy.ExponentialBackoff; +import com.azure.core.http.policy.RetryPolicy; +import com.azure.core.http.policy.RetryStrategy; +import com.azure.data.appconfiguration.ConfigurationClientBuilder; +import com.azure.identity.ManagedIdentityCredentialBuilder; +import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties; +import com.azure.spring.cloud.autoconfigure.implementation.appconfiguration.AzureAppConfigurationProperties; +import com.azure.spring.cloud.config.ConfigurationClientCustomizer; +import com.azure.spring.cloud.config.implementation.pipline.policies.BaseAppConfigurationPolicy; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; +import com.azure.spring.cloud.service.implementation.appconfiguration.ConfigurationClientBuilderFactory; + +public class AppConfigurationReplicaClientsBuilder implements EnvironmentAware { + + private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationReplicaClientsBuilder.class); + + /** + * Invalid Connection String error message + */ + public static final String NON_EMPTY_MSG = "%s property should not be null or empty in the connection string of Azure Config Service."; + + public static final String RETRY_MODE_PROPERTY_NAME = "retry.mode"; + + public static final String MAX_RETRIES_PROPERTY_NAME = "retry.exponential.max-retries"; + + public static final String BASE_DELAY_PROPERTY_NAME = "retry.exponential.base-delay"; + + public static final String MAX_DELAY_PROPERTY_NAME = "retry.exponential.max-delay"; + + private static final Duration DEFAULT_MIN_RETRY_POLICY = Duration.ofMillis(800); + + private static final Duration DEFAULT_MAX_RETRY_POLICY = Duration.ofSeconds(8); + + /** + * Connection String Regex format + */ + private static final String CONN_STRING_REGEXP = "Endpoint=([^;]+);Id=([^;]+);Secret=([^;]+)"; + + /** + * Invalid Formatted Connection String Error message + */ + public static final String ENDPOINT_ERR_MSG = String.format("Connection string does not follow format %s.", + CONN_STRING_REGEXP); + + private static final Pattern CONN_STRING_PATTERN = Pattern.compile(CONN_STRING_REGEXP); + + private ConfigurationClientCustomizer clientProvider; + + private final ConfigurationClientBuilderFactory clientFactory; + + private Environment env; + + private boolean isDev = false; + + private boolean isKeyVaultConfigured = false; + + private final boolean credentialConfigured; + + private final int defaultMaxRetries; + + public AppConfigurationReplicaClientsBuilder(int defaultMaxRetries, + ConfigurationClientBuilderFactory clientFactory, boolean credentialConfigured) { + this.defaultMaxRetries = defaultMaxRetries; + this.clientFactory = clientFactory; + this.credentialConfigured = credentialConfigured; + } + + /** + * Given a connection string, returns the endpoint inside of it. + * + * @param connectionString connection string to app configuration + * @return endpoint + * @throws IllegalStateException when connection string isn't valid. + */ + public static String getEndpointFromConnectionString(String connectionString) { + Assert.hasText(connectionString, "Connection string cannot be empty."); + + Matcher matcher = CONN_STRING_PATTERN.matcher(connectionString); + if (!matcher.find()) { + throw new IllegalStateException(ENDPOINT_ERR_MSG); + } + + String endpoint = matcher.group(1); + + Assert.hasText(endpoint, String.format(NON_EMPTY_MSG, "Endpoint")); + + return endpoint; + } + + /** + * @param clientProvider the clientProvider to set + */ + public void setClientProvider(ConfigurationClientCustomizer clientProvider) { + this.clientProvider = clientProvider; + } + + public void setIsKeyVaultConfigured(boolean isKeyVaultConfigured) { + this.isKeyVaultConfigured = isKeyVaultConfigured; + } + + /** + * Builds all the clients for a connection. + * + * @throws IllegalArgumentException when more than 1 connection method is given. + */ + List buildClients(ConfigStore configStore) { + List clients = new ArrayList<>(); + // Single client or Multiple? + // If single call buildClient + int hasSingleConnectionString = StringUtils.hasText(configStore.getConnectionString()) ? 1 : 0; + int hasMultiEndpoints = configStore.getEndpoints().size() > 0 ? 1 : 0; + int hasMultiConnectionString = configStore.getConnectionStrings().size() > 0 ? 1 : 0; + + if (hasSingleConnectionString + hasMultiEndpoints + hasMultiConnectionString > 1) { + throw new IllegalArgumentException( + "More than 1 Connection method was set for connecting to App Configuration."); + } + + boolean connectionStringIsPresent = configStore.getConnectionString() != null; + + if (credentialConfigured && connectionStringIsPresent) { + throw new IllegalArgumentException( + "More than 1 Connection method was set for connecting to App Configuration."); + } + + List connectionStrings = configStore.getConnectionStrings(); + List endpoints = configStore.getEndpoints(); + + if (connectionStrings.size() == 0 && StringUtils.hasText(configStore.getConnectionString())) { + connectionStrings.add(configStore.getConnectionString()); + } + + if (endpoints.size() == 0 && StringUtils.hasText(configStore.getEndpoint())) { + endpoints.add(configStore.getEndpoint()); + } + + if (connectionStrings.size() > 0) { + for (String connectionString : connectionStrings) { + String endpoint = getEndpointFromConnectionString(connectionString); + LOGGER.debug("Connecting to " + endpoint + " using Connecting String."); + ConfigurationClientBuilder builder = createBuilderInstance().connectionString(connectionString); + clients.add(modifyAndBuildClient(builder, endpoint, connectionStrings.size() - 1)); + } + } else { + for (String endpoint : endpoints) { + ConfigurationClientBuilder builder = this.createBuilderInstance(); + if (!credentialConfigured) { + // System Assigned Identity. Needs to be checked last as all of the above should + // have an Endpoint. + LOGGER.debug("Connecting to " + endpoint + + " using Azure System Assigned Identity or Azure User Assigned Identity."); + ManagedIdentityCredentialBuilder micBuilder = new ManagedIdentityCredentialBuilder(); + builder.credential(micBuilder.build()); + } + + builder.endpoint(endpoint); + + clients.add(modifyAndBuildClient(builder, endpoint, endpoints.size() - 1)); + } + } + return clients; + } + + private AppConfigurationReplicaClient modifyAndBuildClient(ConfigurationClientBuilder builder, String endpoint, + Integer replicaCount) { + builder.addPolicy(new BaseAppConfigurationPolicy(isDev, isKeyVaultConfigured, replicaCount)); + + if (clientProvider != null) { + clientProvider.setup(builder, endpoint); + } + return new AppConfigurationReplicaClient(endpoint, builder.buildClient()); + } + + @Override + public void setEnvironment(Environment environment) { + for (String profile : environment.getActiveProfiles()) { + if ("dev".equalsIgnoreCase(profile)) { + this.isDev = true; + break; + } + } + this.env = environment; + } + + protected ConfigurationClientBuilder createBuilderInstance() { + RetryStrategy retryStatagy = null; + + String mode = env.getProperty(AzureGlobalProperties.PREFIX + "." + RETRY_MODE_PROPERTY_NAME); + String modeService = env.getProperty(AzureAppConfigurationProperties.PREFIX + "." + RETRY_MODE_PROPERTY_NAME); + + if ("exponential".equals(mode) || "exponential".equals(modeService) || (mode == null && modeService == null)) { + Function checkPropertyInt = parameter -> (Integer.parseInt(parameter)); + Function checkPropertyDuration = parameter -> (DurationStyle.detectAndParse(parameter)); + + int retries = checkProperty(MAX_RETRIES_PROPERTY_NAME, defaultMaxRetries, + " isn't a valid integer, using default value.", checkPropertyInt); + + Duration baseDelay = checkProperty(BASE_DELAY_PROPERTY_NAME, DEFAULT_MIN_RETRY_POLICY, + " isn't a valid Duration, using default value.", checkPropertyDuration); + Duration maxDelay = checkProperty(MAX_DELAY_PROPERTY_NAME, DEFAULT_MAX_RETRY_POLICY, + " isn't a valid Duration, using default value.", checkPropertyDuration); + + retryStatagy = new ExponentialBackoff(retries, baseDelay, maxDelay); + } + + ConfigurationClientBuilder builder = clientFactory.build(); + + if (retryStatagy != null) { + builder.retryPolicy(new RetryPolicy(retryStatagy)); + } + + return builder; + } + + private T checkProperty(String propertyName, T defaultValue, String errMsg, Function fn) { + String envValue = System.getProperty(AzureGlobalProperties.PREFIX + "." + propertyName); + String envServiceValue = System.getProperty(AzureAppConfigurationProperties.PREFIX + "." + propertyName); + T value = defaultValue; + + if (envServiceValue != null) { + try { + value = fn.apply(envServiceValue); + } catch (Exception e) { + LOGGER.warn("{}.{} {}", AzureAppConfigurationProperties.PREFIX, propertyName, errMsg); + } + } else if (envValue != null) { + try { + value = fn.apply(envValue); + } catch (Exception e) { + LOGGER.warn("{}.{} {}", AzureGlobalProperties.PREFIX, propertyName, errMsg); + } + } + + return value; + } +} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationStatusException.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationStatusException.java similarity index 99% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationStatusException.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationStatusException.java index ebdd76394b2e9..e762d3860ced3 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationStatusException.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationStatusException.java @@ -13,6 +13,6 @@ class AppConfigurationStatusException extends HttpResponseException { private static final long serialVersionUID = -2388602959090868645L; - + } diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/BackoffTimeCalculator.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/BackoffTimeCalculator.java similarity index 82% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/BackoffTimeCalculator.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/BackoffTimeCalculator.java index dfa83d75f806d..04716600c9b08 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/BackoffTimeCalculator.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/BackoffTimeCalculator.java @@ -20,21 +20,20 @@ final class BackoffTimeCalculator { private static Long minBackoff = (long) 30; /** - * + * * @param maxBackoff maximum amount of time between requests * @param minBackoff minimum amount of time between requests */ - static void setDefaults(Long maxBackoffValue, Long minBackoffValue) { - maxBackoff = maxBackoffValue; - minBackoff = minBackoffValue; + static void setDefaults(Long maxBackoff, Long minBackoff) { + BackoffTimeCalculator.maxBackoff = maxBackoff; + BackoffTimeCalculator.minBackoff = minBackoff; } /** * Calculates the new Backoff time for requests. - * * @param attempts Number of attempts so far * @return Nano Seconds to the next request - * @throws IllegalArgumentException when backofftime or attempt number is invalid + * @throws IllegalArgumentException when back off time or attempt number is invalid */ static Long calculateBackoff(Integer attempts) { @@ -50,8 +49,8 @@ static Long calculateBackoff(Integer attempts) { throw new IllegalArgumentException("Number of previous attempts needs to be a positive number."); } - Long minBackoffNano = minBackoff * SECONDS_TO_NANO_SECONDS; - Long maxBackoffNano = maxBackoff * SECONDS_TO_NANO_SECONDS; + long minBackoffNano = minBackoff * SECONDS_TO_NANO_SECONDS; + long maxBackoffNano = maxBackoff * SECONDS_TO_NANO_SECONDS; if (attempts <= 1 || maxBackoff <= minBackoff) { return minBackoffNano; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/ConnectionManager.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/ConnectionManager.java similarity index 88% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/ConnectionManager.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/ConnectionManager.java index 7465dca321996..45d27befb41cf 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/ConnectionManager.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/ConnectionManager.java @@ -10,10 +10,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.azure.spring.cloud.config.health.AppConfigurationStoreHealth; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreMonitoring; -import com.azure.spring.cloud.config.properties.ConfigStore; -import com.azure.spring.cloud.config.properties.FeatureFlagStore; +import com.azure.spring.cloud.config.implementation.health.AppConfigurationStoreHealth; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreMonitoring; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; +import com.azure.spring.cloud.config.implementation.properties.FeatureFlagStore; /** * Holds a set of connections to an app configuration store with zero to many geo-replications. @@ -37,6 +37,7 @@ public class ConnectionManager { /** * Creates a set of connections to an app configuration store. + * @param clientBuilder Builder for App Configuration Clients * @param configStore Connection info for the store */ ConnectionManager(AppConfigurationReplicaClientsBuilder clientBuilder, ConfigStore configStore) { @@ -91,7 +92,6 @@ List getAvailableClients(Boolean useCurrent) { boolean foundCurrent = !useCurrent; if (clients.size() == 1) { - // If only one client was setup it isn't backed off and always available. availableClients.add(clients.get(0)); } else if (clients.size() > 0) { for (AppConfigurationReplicaClient replicaClient : clients) { @@ -114,7 +114,7 @@ List getAvailableClients(Boolean useCurrent) { } List getAllEndpoints() { - return clients.stream().map(client -> client.getEndpoint()).collect(Collectors.toList()); + return clients.stream().map(AppConfigurationReplicaClient::getEndpoint).collect(Collectors.toList()); } /** @@ -133,7 +133,7 @@ void backoffClient(String endpoint) { } /** - * Updates the sync token of the client. + * Updates the sync token of the client. Only works if no replicas are being used. * * @param syncToken App Configuration sync token */ @@ -141,12 +141,13 @@ void updateSyncToken(String endpoint, String syncToken) { clients.stream().filter(client -> client.getEndpoint().equals(endpoint)).findFirst() .ifPresent(client -> client.updateSyncToken(syncToken)); } - + AppConfigurationStoreMonitoring getMonitoring() { return configStore.getMonitoring(); } - + FeatureFlagStore getFeatureFlagStore() { return configStore.getFeatureFlags(); } + } diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/HostType.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/HostType.java similarity index 93% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/HostType.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/HostType.java index eb1e40a0e366f..604dfa2165f97 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/HostType.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/HostType.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config; +package com.azure.spring.cloud.config.implementation; /** * The Types of Hosts checked in request tracing. diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/JsonConfigurationParser.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/JsonConfigurationParser.java similarity index 96% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/JsonConfigurationParser.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/JsonConfigurationParser.java index 11ca70e6f825a..0cefad2734ce9 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/JsonConfigurationParser.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/JsonConfigurationParser.java @@ -45,13 +45,13 @@ static boolean isJsonContentType(String contentType) { static Map parseJsonSetting(ConfigurationSetting setting) throws JsonProcessingException { - HashMap settings = new HashMap<>(); + Map settings = new HashMap<>(); JsonNode json = MAPPER.readTree(setting.getValue()); parseSetting(setting.getKey(), json, settings); return settings; } - static void parseSetting(String currentKey, JsonNode currentValue, HashMap settings) { + static void parseSetting(String currentKey, JsonNode currentValue, Map settings) { switch (currentValue.getNodeType()) { case ARRAY: for (int i = 0; i < currentValue.size(); i++) { diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/NormalizeNull.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/NormalizeNull.java similarity index 93% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/NormalizeNull.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/NormalizeNull.java index 6d536c432aecb..60b3c5c89f6a1 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/NormalizeNull.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/NormalizeNull.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config; +package com.azure.spring.cloud.config.implementation; import com.azure.data.appconfiguration.models.ConfigurationSetting; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/RequestTracingConstants.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/RequestTracingConstants.java similarity index 96% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/RequestTracingConstants.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/RequestTracingConstants.java index 6217cc0c2605b..3693a0c09a6b2 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/RequestTracingConstants.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/RequestTracingConstants.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config; +package com.azure.spring.cloud.config.implementation; /** * Request Tracing values used to check diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/RequestType.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/RequestType.java similarity index 92% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/RequestType.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/RequestType.java index cc97a0039a5c9..2658dad3bdc48 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/RequestType.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/RequestType.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config; +package com.azure.spring.cloud.config.implementation; /** * The types of requests made to the App Configuration service. diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/State.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/State.java similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/State.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/State.java diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/StateHolder.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/StateHolder.java similarity index 86% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/StateHolder.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/StateHolder.java index 49daa12a6a63e..97460a2b488a8 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/StateHolder.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/StateHolder.java @@ -126,17 +126,25 @@ Map getLoadState() { } /** - * @param originEndpoint the loadState name to set + * @param originEndpoint the configuration store connected to. + * @param loaded true if the configuration store was loaded. + * @param failFast application started after it failed to load from a store. */ - void setLoadState(String originEndpoint, Boolean loaded) { - loadState.put(originEndpoint, loaded); + void setLoadState(String originEndpoint, Boolean loaded, Boolean failFast) { + if (loaded || !failFast) { + loadState.put(originEndpoint, true); + } else { + loadState.put(originEndpoint, false); + } } /** - * @param originEndpoint the loadState feature flag name to set + * @param originEndpoint the configuration store connected to. + * @param loaded true if the configuration store was loaded and uses feature flags. + * @param failFast application started after it failed to load from a store. */ - void setLoadStateFeatureFlag(String originEndpoint, Boolean loaded) { - setLoadState(originEndpoint + FEATURE_ENDPOINT, loaded); + void setLoadStateFeatureFlag(String originEndpoint, Boolean loaded, Boolean failFast) { + setLoadState(originEndpoint + FEATURE_ENDPOINT, loaded, failFast); } /** @@ -160,12 +168,12 @@ public void setNextForcedRefresh(Duration refreshPeriod) { * Sets a minimum value until the next refresh. If a refresh interval has passed or is smaller than the calculated * backoff time, the refresh interval is set to the backoff time. * @param refreshInterval period between refresh checks. - * @param defaultMinBackoff min backoff between checks + * @param defaultMinBackoff min backoff between checks */ void updateNextRefreshTime(Duration refreshInterval, Long defaultMinBackoff) { if (refreshInterval != null) { - Instant newForcedRefresh = getNextRefreshCheck(nextForcedRefresh, clientRefreshAttempts, - refreshInterval.getSeconds(), defaultMinBackoff); + Instant newForcedRefresh = getNextRefreshCheck(nextForcedRefresh, + clientRefreshAttempts, refreshInterval.getSeconds(), defaultMinBackoff); if (newForcedRefresh.compareTo(nextForcedRefresh) != 0) { clientRefreshAttempts += 1; @@ -175,8 +183,8 @@ void updateNextRefreshTime(Duration refreshInterval, Long defaultMinBackoff) { for (Entry entry : state.entrySet()) { State state = entry.getValue(); - Instant newRefresh = getNextRefreshCheck(state.getNextRefreshCheck(), state.getRefreshAttempt(), - (long) state.getRefreshInterval(), defaultMinBackoff); + Instant newRefresh = getNextRefreshCheck(state.getNextRefreshCheck(), + state.getRefreshAttempt(), (long) state.getRefreshInterval(), defaultMinBackoff); if (newRefresh.compareTo(entry.getValue().getNextRefreshCheck()) != 0) { state.incrementRefreshAttempt(); diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationAutoConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationAutoConfiguration.java similarity index 80% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationAutoConfiguration.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationAutoConfiguration.java index 6c7cb3d3a75d1..d2792e1195b23 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationAutoConfiguration.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config; +package com.azure.spring.cloud.config.implementation.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -10,10 +10,11 @@ import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; +import com.azure.spring.cloud.config.AppConfigurationRefresh; import com.azure.spring.cloud.config.implementation.AppConfigurationPullRefresh; import com.azure.spring.cloud.config.implementation.AppConfigurationReplicaClientFactory; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationProviderProperties; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProviderProperties; /** * Setup AppConfigurationRefresh when spring.cloud.azure.appconfiguration.enabled is enabled. @@ -28,7 +29,7 @@ public class AppConfigurationAutoConfiguration { */ @Configuration @ConditionalOnClass(RefreshEndpoint.class) - static class AppConfigurationWatchAutoConfiguration { + public static class AppConfigurationWatchAutoConfiguration { @Bean @ConditionalOnMissingBean diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java new file mode 100644 index 0000000000000..d405fb2229cd6 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation.config; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +import com.azure.data.appconfiguration.ConfigurationClientBuilder; +import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties; +import com.azure.spring.cloud.autoconfigure.implementation.appconfiguration.AzureAppConfigurationProperties; +import com.azure.spring.cloud.autoconfigure.implementation.keyvault.secrets.properties.AzureKeyVaultSecretProperties; +import com.azure.spring.cloud.autoconfigure.implementation.properties.core.AbstractAzureHttpConfigurationProperties; +import com.azure.spring.cloud.autoconfigure.implementation.properties.utils.AzureGlobalPropertiesUtils; +import com.azure.spring.cloud.autoconfigure.properties.core.authentication.TokenCredentialConfigurationProperties; +import com.azure.spring.cloud.config.ConfigurationClientCustomizer; +import com.azure.spring.cloud.config.KeyVaultSecretProvider; +import com.azure.spring.cloud.config.SecretClientCustomizer; +import com.azure.spring.cloud.config.implementation.AppConfigurationKeyVaultClientFactory; +import com.azure.spring.cloud.config.implementation.AppConfigurationPropertySourceLocator; +import com.azure.spring.cloud.config.implementation.AppConfigurationReplicaClientFactory; +import com.azure.spring.cloud.config.implementation.AppConfigurationReplicaClientsBuilder; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProviderProperties; +import com.azure.spring.cloud.core.customizer.AzureServiceClientBuilderCustomizer; +import com.azure.spring.cloud.core.implementation.util.AzurePropertiesUtils; +import com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier; +import com.azure.spring.cloud.service.implementation.appconfiguration.ConfigurationClientBuilderFactory; +import com.azure.spring.cloud.service.implementation.keyvault.secrets.SecretClientBuilderFactory; + +/** + * Setup ConnectionPool, AppConfigurationPropertySourceLocator, and ClientStore when + * spring.cloud.azure.appconfiguration.enabled is enabled. + */ +@Configuration +@EnableConfigurationProperties({ AppConfigurationProperties.class, AppConfigurationProviderProperties.class }) +@ConditionalOnClass(AppConfigurationPropertySourceLocator.class) +@ConditionalOnProperty(prefix = AppConfigurationProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true) +public class AppConfigurationBootstrapConfiguration { + + @Autowired + private transient ApplicationContext context; + + /** + * + * @param properties Client properties + * @param appProperties Library properties + * @param clientFactory Store Connections + * @param keyVaultClientFactory keyVaultClientFactory + * @return AppConfigurationPropertySourceLocator + * @throws IllegalArgumentException if both KeyVaultClientProvider and KeyVaultSecretProvider exist. + */ + @Bean + AppConfigurationPropertySourceLocator sourceLocator(AppConfigurationProperties properties, + AppConfigurationProviderProperties appProperties, AppConfigurationReplicaClientFactory clientFactory, + AppConfigurationKeyVaultClientFactory keyVaultClientFactory) + throws IllegalArgumentException { + + return new AppConfigurationPropertySourceLocator(appProperties, clientFactory, keyVaultClientFactory, + properties.getRefreshInterval(), properties.getStores()); + } + + /** + * @param clientProperties AzureKeyVaultSecretProvider client properties + * @throws IllegalArgumentException if both KeyVaultClientProvider and KeyVaultSecretProvider exist. + */ + @Bean + AppConfigurationKeyVaultClientFactory keyVaultClientFactory(Environment environment) + throws IllegalArgumentException { + AzureGlobalProperties globalSource = Binder.get(environment).bindOrCreate(AzureGlobalProperties.PREFIX, + AzureGlobalProperties.class); + AzureGlobalProperties serviceSource = Binder.get(environment).bindOrCreate(AzureKeyVaultSecretProperties.PREFIX, + AzureGlobalProperties.class); + + AzureKeyVaultSecretProperties globalProperties = AzureGlobalPropertiesUtils.loadProperties( + globalSource, + new AzureKeyVaultSecretProperties()); + AzureKeyVaultSecretProperties clientProperties = AzureGlobalPropertiesUtils.loadProperties(serviceSource, + new AzureKeyVaultSecretProperties()); + + AzurePropertiesUtils.copyAzureCommonPropertiesIgnoreNull(globalProperties, clientProperties); + + SecretClientCustomizer keyVaultClientProvider = context.getBeanProvider(SecretClientCustomizer.class) + .getIfAvailable(); + KeyVaultSecretProvider keyVaultSecretProvider = context.getBeanProvider(KeyVaultSecretProvider.class) + .getIfAvailable(); + + SecretClientBuilderFactory secretClientBuilderFactory = new SecretClientBuilderFactory(clientProperties); + + boolean credentialConfigured = isCredentialConfigured(clientProperties); + + return new AppConfigurationKeyVaultClientFactory(keyVaultClientProvider, keyVaultSecretProvider, secretClientBuilderFactory, credentialConfigured); + } + + /** + * Factory for working with App Configuration Clients + * + * @param clientBuilder Builder for configuration clients + * @param properties Client configurations for setting up connections to each config store. + * @return AppConfigurationReplicaClientFactory + */ + @Bean + @ConditionalOnMissingBean + AppConfigurationReplicaClientFactory buildClientFactory(AppConfigurationReplicaClientsBuilder clientBuilder, + AppConfigurationProperties properties) { + return new AppConfigurationReplicaClientFactory(clientBuilder, properties.getStores()); + } + + /** + * Builder for clients connecting to App Configuration. + * + * @param clientProperties AzureAppConfigurationProperties Spring Cloud Azure global properties. + * @param appProperties Library configurations for setting up connections to each config store. + * @param keyVaultClientFactory used for tracing info for if key vault has been configured + * @param customizers Client Customizers for connecting to Azure App Configuration + * @return ClientStore + */ + @Bean + @ConditionalOnMissingBean + AppConfigurationReplicaClientsBuilder replicaClientBuilder(Environment environment, + AppConfigurationProviderProperties appProperties, AppConfigurationKeyVaultClientFactory keyVaultClientFactory, + ObjectProvider> customizers) { + AzureGlobalProperties globalSource = Binder.get(environment).bindOrCreate(AzureGlobalProperties.PREFIX, + AzureGlobalProperties.class); + AzureGlobalProperties serviceSource = Binder.get(environment).bindOrCreate( + AzureAppConfigurationProperties.PREFIX, + AzureGlobalProperties.class); + + AzureGlobalProperties globalProperties = AzureGlobalPropertiesUtils.loadProperties(globalSource, + new AzureGlobalProperties()); + AzureAppConfigurationProperties clientProperties = AzureGlobalPropertiesUtils.loadProperties(serviceSource, + new AzureAppConfigurationProperties()); + + AzurePropertiesUtils.copyAzureCommonPropertiesIgnoreNull(globalProperties, clientProperties); + + ConfigurationClientBuilderFactory clientFactory = new ConfigurationClientBuilderFactory(clientProperties); + + clientFactory.setSpringIdentifier(AzureSpringIdentifier.AZURE_SPRING_APP_CONFIG); + customizers.orderedStream().forEach(clientFactory::addBuilderCustomizer); + + boolean credentialConfigured = isCredentialConfigured(clientProperties); + + AppConfigurationReplicaClientsBuilder clientBuilder = new AppConfigurationReplicaClientsBuilder( + appProperties.getMaxRetries(), clientFactory, credentialConfigured); + + clientBuilder + .setClientProvider(context.getBeanProvider(ConfigurationClientCustomizer.class) + .getIfAvailable()); + + clientBuilder.setIsKeyVaultConfigured(keyVaultClientFactory.isConfigured()); + + return clientBuilder; + } + + private boolean isCredentialConfigured(AbstractAzureHttpConfigurationProperties properties) { + if (properties.getCredential() != null) { + TokenCredentialConfigurationProperties tokenProps = properties.getCredential(); + if (StringUtils.hasText(tokenProps.getClientCertificatePassword())) { + return true; + } else if (StringUtils.hasText(tokenProps.getClientCertificatePath())) { + return true; + } else if (StringUtils.hasText(tokenProps.getClientId())) { + return true; + } else if (StringUtils.hasText(tokenProps.getClientSecret())) { + return true; + } else if (StringUtils.hasText(tokenProps.getUsername())) { + return true; + } else if (StringUtils.hasText(tokenProps.getPassword())) { + return true; + } + } + + return false; + } + + + +} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/feature/management/entity/Feature.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/feature/management/entity/Feature.java similarity index 95% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/feature/management/entity/Feature.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/feature/management/entity/Feature.java index 630e49548f264..f0b7e96991240 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/feature/management/entity/Feature.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/feature/management/entity/Feature.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.feature.management.entity; +package com.azure.spring.cloud.config.implementation.feature.management.entity; import java.util.HashMap; import java.util.List; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/feature/management/entity/FeatureSet.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/feature/management/entity/FeatureSet.java similarity index 93% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/feature/management/entity/FeatureSet.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/feature/management/entity/FeatureSet.java index c3006b5241cbd..e34851daefb4e 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/feature/management/entity/FeatureSet.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/feature/management/entity/FeatureSet.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.feature.management.entity; +package com.azure.spring.cloud.config.implementation.feature.management.entity; import java.util.HashMap; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/health/AppConfigurationStoreHealth.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/health/AppConfigurationStoreHealth.java similarity index 87% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/health/AppConfigurationStoreHealth.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/health/AppConfigurationStoreHealth.java index e2594a8035401..ec7fc02612f93 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/health/AppConfigurationStoreHealth.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/health/AppConfigurationStoreHealth.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.health; +package com.azure.spring.cloud.config.implementation.health; /** * App Configuration Health states diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/pipline/policies/BaseAppConfigurationPolicy.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/pipline/policies/BaseAppConfigurationPolicy.java similarity index 89% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/pipline/policies/BaseAppConfigurationPolicy.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/pipline/policies/BaseAppConfigurationPolicy.java index 927f14ae06b7b..76ce81bade122 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/pipline/policies/BaseAppConfigurationPolicy.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/pipline/policies/BaseAppConfigurationPolicy.java @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.pipline.policies; +package com.azure.spring.cloud.config.implementation.pipline.policies; -import static com.azure.spring.cloud.config.AppConfigurationConstants.DEV_ENV_TRACING; -import static com.azure.spring.cloud.config.AppConfigurationConstants.KEY_VAULT_CONFIGURED_TRACING; -import static com.azure.spring.cloud.config.AppConfigurationConstants.USER_AGENT_TYPE; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.DEV_ENV_TRACING; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.KEY_VAULT_CONFIGURED_TRACING; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.USER_AGENT_TYPE; import org.springframework.util.StringUtils; @@ -13,9 +13,9 @@ import com.azure.core.http.HttpRequest; import com.azure.core.http.HttpResponse; import com.azure.core.http.policy.HttpPipelinePolicy; -import com.azure.spring.cloud.config.HostType; -import com.azure.spring.cloud.config.RequestTracingConstants; -import com.azure.spring.cloud.config.RequestType; +import com.azure.spring.cloud.config.implementation.HostType; +import com.azure.spring.cloud.config.implementation.RequestTracingConstants; +import com.azure.spring.cloud.config.implementation.RequestType; import reactor.core.publisher.Mono; /** diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationKeyValueSelector.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationKeyValueSelector.java new file mode 100644 index 0000000000000..662ece0d6718e --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationKeyValueSelector.java @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation.properties; + +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.EMPTY_LABEL; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import javax.annotation.PostConstruct; +import javax.validation.constraints.NotNull; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Properties on what Selects are checked before loading configurations. + */ +public final class AppConfigurationKeyValueSelector { + + /** + * Label for requesting all configurations with (No Label) + */ + private static final String[] EMPTY_LABEL_ARRAY = { EMPTY_LABEL }; + + private static final String APPLICATION_SETTING_DEFAULT_KEY_FILTER = "/application/"; + + /** + * Separator for multiple labels + */ + public static final String LABEL_SEPARATOR = ","; + + @NotNull + private String keyFilter = ""; + + private String labelFilter; + + /** + * @return the keyFilter + */ + public String getKeyFilter() { + return StringUtils.hasText(keyFilter) ? keyFilter : APPLICATION_SETTING_DEFAULT_KEY_FILTER; + } + + /** + * @param keyFilter the keyFilter to set + * @return AppConfigurationStoreSelects + */ + public AppConfigurationKeyValueSelector setKeyFilter(String keyFilter) { + this.keyFilter = keyFilter; + return this; + } + + /** + * @param profiles List of current Spring profiles to default to using is null label is set. + * @return List of reversed label values, which are split by the separator, the latter label has higher priority + */ + public String[] getLabelFilter(List profiles) { + if (labelFilter == null && profiles.size() > 0) { + Collections.reverse(profiles); + return profiles.toArray(new String[profiles.size()]); + } else if (!StringUtils.hasText(labelFilter)) { + return EMPTY_LABEL_ARRAY; + } + + // The use of trim makes label= dev,prod and label= dev, prod equal. + List labels = Arrays.stream(labelFilter.split(LABEL_SEPARATOR)) + .map(this::mapLabel) + .distinct() + .collect(Collectors.toList()); + + if (labelFilter.endsWith(",")) { + labels.add(EMPTY_LABEL); + } + + Collections.reverse(labels); + String[] t = new String[labels.size()]; + return labels.toArray(t); + } + + /** + * @param labelFilter the labelFilter to set + * @return AppConfigurationStoreSelects + */ + public AppConfigurationKeyValueSelector setLabelFilter(String labelFilter) { + this.labelFilter = labelFilter; + return this; + } + + /** + * Validates key-filter and label-filter are valid. + */ + @PostConstruct + public void validateAndInit() { + Assert.isTrue(!keyFilter.contains("*"), "KeyFilter must not contain asterisk(*)"); + if (labelFilter != null) { + Assert.isTrue(!labelFilter.contains("*"), "LabelFilter must not contain asterisk(*)"); + } + } + + private String mapLabel(String label) { + if (label == null || "".equals(label) || EMPTY_LABEL.equals(label)) { + return EMPTY_LABEL; + } + return label.trim(); + } +} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationProperties.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProperties.java similarity index 60% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationProperties.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProperties.java index 8d129cf7293da..2c1829556f797 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationProperties.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProperties.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.properties; +package com.azure.spring.cloud.config.implementation.properties; import java.time.Duration; import java.util.ArrayList; @@ -9,18 +9,14 @@ import java.util.Map; import javax.annotation.PostConstruct; -import javax.validation.constraints.NotEmpty; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.context.annotation.Import; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.validation.annotation.Validated; -import com.azure.spring.cloud.config.resource.AppConfigManagedIdentityProperties; - /** * Properties for all Azure App Configuration stores that are loaded. */ @@ -33,31 +29,13 @@ public final class AppConfigurationProperties { * Prefix for client configurations for connecting to configuration stores. */ public static final String CONFIG_PREFIX = "spring.cloud.azure.appconfiguration"; - - /** - * Separator for multiple labels. - */ - public static final String LABEL_SEPARATOR = ","; - - /** - * Context for loading configuration keys. - */ - @NotEmpty - private String defaultContext = "application"; private boolean enabled = true; private List stores = new ArrayList<>(); - /** - * Alternative to Spring application name, if not configured, fallback to default Spring application name - **/ - private String name; - - @NestedConfigurationProperty - private AppConfigManagedIdentityProperties managedIdentity; - - private boolean pushRefresh = true; + @Nullable + private String clientId; // Optional: client_id of the managed identity private Duration refreshInterval; @@ -90,72 +68,17 @@ public void setStores(List stores) { } /** - * The prefixed used before all keys loaded. - * @deprecated Use spring.cloud.azure.appconfiguration[0].selects - * @return null - */ - @Deprecated - public String getDefaultContext() { - return defaultContext; - } - - /** - * Overrides the default context of `application`. - * @deprecated Use spring.cloud.azure.appconfiguration[0].selects - * @param defaultContext Key Prefix. - */ - @Deprecated - public void setDefaultContext(String defaultContext) { - this.defaultContext = defaultContext; - } - - /** - * Used to override the spring.application.name value - * @deprecated Use spring.cloud.azure.appconfiguration[0].selects - * @return name - */ - @Deprecated - @Nullable - public String getName() { - return name; - } - - /** - * Used to override the spring.application.name value - * @deprecated Use spring.cloud.azure.appconfiguration[0].selects - * @param name application name in config key. - */ - @Deprecated - public void setName(@Nullable String name) { - this.name = name; - } - - /** - * @return the managedIdentity - */ - public AppConfigManagedIdentityProperties getManagedIdentity() { - return managedIdentity; - } - - /** - * @param managedIdentity the managedIdentity to set - */ - public void setManagedIdentity(AppConfigManagedIdentityProperties managedIdentity) { - this.managedIdentity = managedIdentity; - } - - /** - * @return the pushRefresh + * @return the clientId */ - public Boolean getPushRefresh() { - return pushRefresh; + public String getClientId() { + return clientId; } /** - * @param pushRefresh the pushRefresh to set + * @param clientId the clientId to set */ - public void setPushRefresh(Boolean pushRefresh) { - this.pushRefresh = pushRefresh; + public void setClientId(String clientId) { + this.clientId = clientId; } /** @@ -173,7 +96,7 @@ public void setRefreshInterval(Duration refreshInterval) { } /** - * Validates at least one store is configured for use, and they are valid. + * Validates at least one store is configured for use, and that they are valid. * @throws IllegalArgumentException when duplicate endpoints are configured */ @PostConstruct diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationProviderProperties.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProviderProperties.java similarity index 95% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationProviderProperties.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProviderProperties.java index c6db7dc0f470d..5a54562d03d55 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationProviderProperties.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProviderProperties.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.properties; +package com.azure.spring.cloud.config.implementation.properties; import java.time.Instant; @@ -26,7 +26,8 @@ public class AppConfigurationProviderProperties { * Prefix for the libraries internal configurations. */ public static final String CONFIG_PREFIX = "spring.cloud.appconfiguration"; - private static final Instant startDate = Instant.now(); + + private static final Instant START_DATE = Instant.now(); @NotEmpty @Value("${version:1.0}") @@ -112,7 +113,7 @@ public void setPrekillTime(int prekillTime) { * @return the startDate */ public Instant getStartDate() { - return startDate; + return START_DATE; } /** diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationStoreMonitoring.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationStoreMonitoring.java similarity index 98% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationStoreMonitoring.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationStoreMonitoring.java index 2f07f99b39938..91b713c35801e 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationStoreMonitoring.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationStoreMonitoring.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.properties; +package com.azure.spring.cloud.config.implementation.properties; import java.time.Duration; import java.util.ArrayList; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationStoreTrigger.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationStoreTrigger.java similarity index 80% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationStoreTrigger.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationStoreTrigger.java index c3f35d9c0b111..995a4d106b940 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationStoreTrigger.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationStoreTrigger.java @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.properties; +package com.azure.spring.cloud.config.implementation.properties; + +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.EMPTY_LABEL; import javax.annotation.PostConstruct; import javax.validation.constraints.NotNull; import org.springframework.util.Assert; -import static com.azure.spring.cloud.config.AppConfigurationConstants.EMPTY_LABEL; /** * Properties on what Triggers are checked before a refresh is triggered. @@ -54,14 +55,6 @@ public void validateAndInit() { Assert.notNull(key, "All Triggers need a key value set."); } - @Override - public String toString() { - if (label == null) { - return key + "/"; - } - return key + "/" + label; - } - private String mapLabel(String label) { if (label == null || "".equals(label)) { return EMPTY_LABEL; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/ConfigStore.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/ConfigStore.java similarity index 90% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/ConfigStore.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/ConfigStore.java index d232a996a82a0..dce7b6e411252 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/ConfigStore.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/ConfigStore.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.properties; +package com.azure.spring.cloud.config.implementation.properties; import java.net.URI; import java.net.URISyntaxException; @@ -26,10 +26,10 @@ public final class ConfigStore { private String connectionString; - private final List connectionStrings = new ArrayList<>(); + private List connectionStrings = new ArrayList<>(); // Label values separated by comma in the Azure Config Service, can be empty - private List selects = new ArrayList<>(); + private List selects = new ArrayList<>(); private boolean failFast = true; @@ -119,14 +119,14 @@ public void setEnabled(boolean enabled) { /** * @return the selects */ - public List getSelects() { + public List getSelects() { return selects; } /** * @param selects the selects to set */ - public void setSelects(List selects) { + public void setSelects(List selects) { this.selects = selects; } @@ -164,10 +164,10 @@ public void setFeatureFlags(FeatureFlagStore featureFlags) { @PostConstruct public void validateAndInit() { if (selects.isEmpty()) { - selects.add(new AppConfigurationStoreSelects().setKeyFilter(DEFAULT_KEYS)); + selects.add(new AppConfigurationKeyValueSelector().setKeyFilter(DEFAULT_KEYS)); } - for (AppConfigurationStoreSelects selectedKeys : selects) { + for (AppConfigurationKeyValueSelector selectedKeys : selects) { selectedKeys.validateAndInit(); } @@ -199,5 +199,6 @@ public void validateAndInit() { } monitoring.validateAndInit(); + featureFlags.validateAndInit(); } } diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationStoreSelects.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagKeyValueSelector.java similarity index 78% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationStoreSelects.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagKeyValueSelector.java index 5f4ddf233aba7..8280fe1bfd313 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/properties/AppConfigurationStoreSelects.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagKeyValueSelector.java @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.properties; +package com.azure.spring.cloud.config.implementation.properties; -import static com.azure.spring.cloud.config.AppConfigurationConstants.EMPTY_LABEL; -import static com.azure.spring.cloud.config.AppConfigurationConstants.LABEL_SEPARATOR; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.EMPTY_LABEL; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.LABEL_SEPARATOR; import java.util.Arrays; import java.util.Collections; @@ -11,7 +11,6 @@ import java.util.stream.Collectors; import javax.annotation.PostConstruct; -import javax.validation.constraints.NotNull; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -19,15 +18,14 @@ /** * Properties on what Selects are checked before loading configurations. */ -public final class AppConfigurationStoreSelects { +public final class FeatureFlagKeyValueSelector { /** * Label for requesting all configurations with (No Label) */ private static final String[] EMPTY_LABEL_ARRAY = { EMPTY_LABEL }; - @NotNull - private String keyFilter = "/application/"; + private String keyFilter = ""; private String labelFilter; @@ -42,7 +40,7 @@ public String getKeyFilter() { * @param keyFilter the keyFilter to set * @return AppConfigurationStoreSelects */ - public AppConfigurationStoreSelects setKeyFilter(String keyFilter) { + public FeatureFlagKeyValueSelector setKeyFilter(String keyFilter) { this.keyFilter = keyFilter; return this; } @@ -83,20 +81,11 @@ public String getLabelFilterText(List profiles) { return String.join(",", getLabelFilter(profiles)); } - /** - * Used for Generating Property Source name only. - * - * @return String all labels combined. - */ - public String getLabel() { - return labelFilter; - } - /** * @param labelFilter the labelFilter to set * @return AppConfigurationStoreSelects */ - public AppConfigurationStoreSelects setLabelFilter(String labelFilter) { + public FeatureFlagKeyValueSelector setLabelFilter(String labelFilter) { this.labelFilter = labelFilter; return this; } @@ -106,7 +95,6 @@ public AppConfigurationStoreSelects setLabelFilter(String labelFilter) { */ @PostConstruct public void validateAndInit() { - Assert.isTrue(!keyFilter.contains("*"), "KeyFilter must not contain asterisk(*)"); if (labelFilter != null) { Assert.isTrue(!labelFilter.contains("*"), "LabelFilter must not contain asterisk(*)"); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagStore.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagStore.java new file mode 100644 index 0000000000000..b1a61b25251ce --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagStore.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation.properties; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.PostConstruct; + +/** + * Properties for what needs to be requested from Azure App Configuration for Feature Flags. + */ +public final class FeatureFlagStore { + + /** + * Boolean for if feature flag loading is enabled. + */ + private Boolean enabled = false; + + private List selects = new ArrayList<>(); + + /** + * @return the enabled + */ + public Boolean getEnabled() { + return enabled; + } + + /** + * @param enabled the enabled to set + */ + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + /** + * @return the selects + */ + public List getSelects() { + return selects; + } + + /** + * @param selects the selects to set + */ + public void setSelects(List selects) { + this.selects = selects; + } + + @PostConstruct + public void validateAndInit() { + if (enabled && selects.size() == 0) { + selects.add(new FeatureFlagKeyValueSelector()); + } + selects.forEach(FeatureFlagKeyValueSelector::validateAndInit); + } + +} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/stores/AppConfigurationSecretClientManager.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/stores/AppConfigurationSecretClientManager.java new file mode 100644 index 0000000000000..c0d9c7927a2c9 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/stores/AppConfigurationSecretClientManager.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation.stores; + +import java.net.URI; +import java.time.Duration; + +import org.springframework.util.StringUtils; + +import com.azure.identity.ManagedIdentityCredentialBuilder; +import com.azure.security.keyvault.secrets.SecretAsyncClient; +import com.azure.security.keyvault.secrets.SecretClientBuilder; +import com.azure.security.keyvault.secrets.models.KeyVaultSecret; +import com.azure.spring.cloud.config.KeyVaultSecretProvider; +import com.azure.spring.cloud.config.SecretClientCustomizer; +import com.azure.spring.cloud.service.implementation.keyvault.secrets.SecretClientBuilderFactory; + +/** + * Client for connecting to and getting secrets from a Key Vault + */ +public final class AppConfigurationSecretClientManager { + + private SecretAsyncClient secretClient; + + private final SecretClientCustomizer keyVaultClientProvider; + + private final String endpoint; + + private final KeyVaultSecretProvider keyVaultSecretProvider; + + private final SecretClientBuilderFactory secretClientFactory; + + private final boolean credentialConfigured; + + /** + * Creates a Client for connecting to Key Vault + * @param endpoint Key Vault endpoint + * @param tokenCredentialProvider optional provider of the Token Credential for connecting to Key Vault + * @param keyVaultClientProvider optional provider for overriding the Key Vault Client + * @param keyVaultSecretProvider optional provider for providing Secrets instead of connecting to Key Vault + * @param authClientId clientId used to authenticate with to App Configuration (Optional) + */ + public AppConfigurationSecretClientManager(String endpoint, SecretClientCustomizer keyVaultClientProvider, + KeyVaultSecretProvider keyVaultSecretProvider, SecretClientBuilderFactory secretClientFactory, boolean credentialConfigured) { + this.endpoint = endpoint; + this.keyVaultClientProvider = keyVaultClientProvider; + this.keyVaultSecretProvider = keyVaultSecretProvider; + this.secretClientFactory = secretClientFactory; + this.credentialConfigured = credentialConfigured; + } + + AppConfigurationSecretClientManager build() { + SecretClientBuilder builder = secretClientFactory.build(); + + if (credentialConfigured) { + // System Assigned Identity. + builder.credential(new ManagedIdentityCredentialBuilder().build()); + } + builder.vaultUrl(endpoint); + + if (keyVaultClientProvider != null) { + keyVaultClientProvider.setup(builder, endpoint); + } + + secretClient = builder.buildAsyncClient(); + + return this; + } + + /** + * Gets the specified secret using the Secret Identifier + * + * @param secretIdentifier The Secret Identifier to Secret + * @param timeout How long it waits for a response from Key Vault + * @return Secret values that matches the secretIdentifier + */ + public KeyVaultSecret getSecret(URI secretIdentifier, int timeout) { + if (secretClient == null) { + build(); + } + + String[] tokens = secretIdentifier.getPath().split("/"); + + String name = (tokens.length >= 3 ? tokens[2] : null); + String version = (tokens.length >= 4 ? tokens[3] : null); + + if (keyVaultSecretProvider != null) { // Secret Resolver + String secret = keyVaultSecretProvider.getSecret(secretIdentifier.getRawPath()); + if (StringUtils.hasText(secret)) { + return new KeyVaultSecret(name, secret); + } + } + + return secretClient.getSecret(name, version).block(Duration.ofSeconds(timeout)); + } + +} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/package-info.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/package-info.java similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/package-info.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/package-info.java diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000000..66ca567069c5c --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories @@ -0,0 +1,6 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.azure.spring.cloud.config.implementation.config.AppConfigurationAutoConfiguration + +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +com.azure.spring.cloud.config.implementation.config.AppConfigurationBootstrapConfiguration + diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/resources/appConfiguration.yaml b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/appConfiguration.yaml similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/resources/appConfiguration.yaml rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/appConfiguration.yaml diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java new file mode 100644 index 0000000000000..6c607fed97ad5 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationApplicationSettingPropertySourceTest.java @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation; + +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_CONN_STRING; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_KEY_1; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_KEY_2; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_KEY_3; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_LABEL_1; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_LABEL_2; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_LABEL_3; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_SLASH_KEY; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_SLASH_VALUE; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_STORE_NAME; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_VALUE_1; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_VALUE_2; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_VALUE_3; +import static com.azure.spring.cloud.config.implementation.TestUtils.createItem; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; + +public class AppConfigurationApplicationSettingPropertySourceTest { + + private static final String EMPTY_CONTENT_TYPE = ""; + + private static final AppConfigurationProperties TEST_PROPS = new AppConfigurationProperties(); + + private static final String KEY_FILTER = "/foo/"; + + private static final ConfigurationSetting ITEM_1 = createItem(KEY_FILTER, TEST_KEY_1, TEST_VALUE_1, TEST_LABEL_1, + EMPTY_CONTENT_TYPE); + + private static final ConfigurationSetting ITEM_2 = createItem(KEY_FILTER, TEST_KEY_2, TEST_VALUE_2, TEST_LABEL_2, + EMPTY_CONTENT_TYPE); + + private static final ConfigurationSetting ITEM_3 = createItem(KEY_FILTER, TEST_KEY_3, TEST_VALUE_3, TEST_LABEL_3, + EMPTY_CONTENT_TYPE); + + private static final ConfigurationSetting ITEM_NULL = createItem(KEY_FILTER, TEST_KEY_3, TEST_VALUE_3, TEST_LABEL_3, + null); + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private List testItems = new ArrayList<>(); + + private AppConfigurationApplicationSettingPropertySource propertySource; + + @Mock + private AppConfigurationReplicaClient clientMock; + + @Mock + private AppConfigurationKeyVaultClientFactory keyVaultClientFactoryMock; + + @Mock + private List configurationListMock; + + @BeforeAll + public static void setup() { + TestUtils.addStore(TEST_PROPS, TEST_STORE_NAME, TEST_CONN_STRING, KEY_FILTER); + } + + @BeforeEach + public void init() { + MAPPER.setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE); + + MockitoAnnotations.openMocks(this); + + testItems = new ArrayList<>(); + testItems.add(ITEM_1); + testItems.add(ITEM_2); + testItems.add(ITEM_3); + + String[] labelFilter = { "\0" }; + + propertySource = new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, clientMock, + keyVaultClientFactoryMock, KEY_FILTER, labelFilter, 60); + } + + @AfterEach + public void cleanup() throws Exception { + MockitoAnnotations.openMocks(this).close(); + } + + @Test + public void testPropCanBeInitAndQueried() throws IOException { + when(configurationListMock.iterator()).thenReturn(testItems.iterator()); + when(clientMock.listSettings(Mockito.any())).thenReturn(configurationListMock) + .thenReturn(configurationListMock); + + propertySource.initProperties(); + + String[] keyNames = propertySource.getPropertyNames(); + String[] expectedKeyNames = testItems.stream() + .map(t -> t.getKey().substring(KEY_FILTER.length())).toArray(String[]::new); + + assertThat(keyNames).containsExactlyInAnyOrder(expectedKeyNames); + + assertThat(propertySource.getProperty(TEST_KEY_1)).isEqualTo(TEST_VALUE_1); + assertThat(propertySource.getProperty(TEST_KEY_2)).isEqualTo(TEST_VALUE_2); + assertThat(propertySource.getProperty(TEST_KEY_3)).isEqualTo(TEST_VALUE_3); + } + + @Test + public void testPropertyNameSlashConvertedToDots() throws IOException { + ConfigurationSetting slashedProp = createItem(KEY_FILTER, TEST_SLASH_KEY, TEST_SLASH_VALUE, null, + EMPTY_CONTENT_TYPE); + List settings = new ArrayList<>(); + settings.add(slashedProp); + when(configurationListMock.iterator()).thenReturn(settings.iterator()) + .thenReturn(Collections.emptyIterator()); + when(clientMock.listSettings(Mockito.any())).thenReturn(configurationListMock) + .thenReturn(configurationListMock); + + propertySource.initProperties(); + + String expectedKeyName = TEST_SLASH_KEY.replace('/', '.'); + String[] actualKeyNames = propertySource.getPropertyNames(); + + assertThat(actualKeyNames.length).isEqualTo(1); + assertThat(actualKeyNames[0]).isEqualTo(expectedKeyName); + assertThat(propertySource.getProperty(TEST_SLASH_KEY)).isNull(); + assertThat(propertySource.getProperty(expectedKeyName)).isEqualTo(TEST_SLASH_VALUE); + } + + @Test + public void initNullValidContentTypeTest() throws IOException { + List items = new ArrayList<>(); + items.add(ITEM_NULL); + when(configurationListMock.iterator()).thenReturn(items.iterator()) + .thenReturn(Collections.emptyIterator()); + when(clientMock.listSettings(Mockito.any())).thenReturn(configurationListMock); + + propertySource.initProperties(); + + String[] keyNames = propertySource.getPropertyNames(); + String[] expectedKeyNames = items.stream() + .map(t -> t.getKey().substring(KEY_FILTER.length())).toArray(String[]::new); + + assertThat(keyNames).containsExactlyInAnyOrder(expectedKeyNames); + } +} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java new file mode 100644 index 0000000000000..de07e656bef18 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationFeatureManagementPropertySourceTest.java @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation; + +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.DEFAULT_ROLLOUT_PERCENTAGE; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.EMPTY_LABEL; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.FEATURE_MANAGEMENT_KEY; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.GROUPS; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.USERS; +import static com.azure.spring.cloud.config.implementation.TestConstants.FEATURE_BOOLEAN_VALUE; +import static com.azure.spring.cloud.config.implementation.TestConstants.FEATURE_LABEL; +import static com.azure.spring.cloud.config.implementation.TestConstants.FEATURE_VALUE; +import static com.azure.spring.cloud.config.implementation.TestConstants.FEATURE_VALUE_PARAMETERS; +import static com.azure.spring.cloud.config.implementation.TestConstants.FEATURE_VALUE_TARGETING; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_CONN_STRING; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_STORE_NAME; +import static com.azure.spring.cloud.config.implementation.TestUtils.createItemFeatureFlag; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; +import com.azure.data.appconfiguration.models.FeatureFlagFilter; +import com.azure.spring.cloud.config.implementation.feature.management.entity.Feature; +import com.azure.spring.cloud.config.implementation.feature.management.entity.FeatureSet; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.config.implementation.properties.FeatureFlagStore; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; + +public class AppConfigurationFeatureManagementPropertySourceTest { + + public static final List FEATURE_ITEMS = new ArrayList<>(); + + public static final List FEATURE_ITEMS_TARGETING = new ArrayList<>(); + + private static final AppConfigurationProperties TEST_PROPS = new AppConfigurationProperties(); + + private static final String KEY_FILTER = "/foo/"; + + private static final FeatureFlagConfigurationSetting FEATURE_ITEM = createItemFeatureFlag(".appconfig.featureflag/", + "Alpha", + FEATURE_VALUE, FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE); + + private static final FeatureFlagConfigurationSetting FEATURE_ITEM_2 = createItemFeatureFlag( + ".appconfig.featureflag/", "Beta", + FEATURE_BOOLEAN_VALUE, FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE); + + private static final FeatureFlagConfigurationSetting FEATURE_ITEM_3 = createItemFeatureFlag( + ".appconfig.featureflag/", "Gamma", + FEATURE_VALUE_PARAMETERS, FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE); + + private static final FeatureFlagConfigurationSetting FEATURE_ITEM_NULL = createItemFeatureFlag( + ".appconfig.featureflag/", "Alpha", + FEATURE_VALUE, + FEATURE_LABEL, null); + + private static final FeatureFlagConfigurationSetting FEATURE_ITEM_TARGETING = createItemFeatureFlag( + ".appconfig.featureflag/", "target", + FEATURE_VALUE_TARGETING, FEATURE_LABEL, FEATURE_FLAG_CONTENT_TYPE); + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private AppConfigurationFeatureManagementPropertySource propertySource; + + @Mock + private AppConfigurationReplicaClient clientMock; + + private FeatureFlagStore featureFlagStore; + + @Mock + private List featureListMock; + + @BeforeAll + public static void setup() { + TestUtils.addStore(TEST_PROPS, TEST_STORE_NAME, TEST_CONN_STRING, KEY_FILTER); + + FEATURE_ITEM.setContentType(FEATURE_FLAG_CONTENT_TYPE); + FEATURE_ITEMS.add(FEATURE_ITEM); + FEATURE_ITEMS.add(FEATURE_ITEM_2); + FEATURE_ITEMS.add(FEATURE_ITEM_3); + + FEATURE_ITEMS_TARGETING.add(FEATURE_ITEM_TARGETING); + } + + @BeforeEach + public void init() { + MAPPER.setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE); + + MockitoAnnotations.openMocks(this); + + featureFlagStore = new FeatureFlagStore(); + + String[] labelFilter = { EMPTY_LABEL }; + + propertySource = new AppConfigurationFeatureManagementPropertySource(TEST_STORE_NAME, clientMock, "", + labelFilter); + } + + @AfterEach + public void cleanup() throws Exception { + MockitoAnnotations.openMocks(this).close(); + } + + @Test + public void testFeatureFlagCanBeInitedAndQueried() { + when(featureListMock.iterator()).thenReturn(FEATURE_ITEMS.iterator()); + when(clientMock.listSettings(Mockito.any())) + .thenReturn(featureListMock).thenReturn(featureListMock); + featureFlagStore.setEnabled(true); + + propertySource.initProperties(); + + HashMap filters = new HashMap<>(); + FeatureFlagFilter ffec = new FeatureFlagFilter("TestFilter"); + filters.put(0, ffec); + Feature gamma = new Feature(); + gamma.setKey("Gamma"); + filters = new HashMap<>(); + ffec = new FeatureFlagFilter("TestFilter"); + LinkedHashMap parameters = new LinkedHashMap<>(); + parameters.put("key", "value"); + ffec.setParameters(parameters); + filters.put(0, ffec); + gamma.setEnabledFor(filters); + + assertEquals(gamma.getKey(), + ((Feature) propertySource.getProperty(FEATURE_MANAGEMENT_KEY + "Gamma")).getKey()); + } + + @Test + public void testFeatureFlagThrowError() { + when(featureListMock.iterator()).thenReturn(FEATURE_ITEMS.iterator()); + when(clientMock.listSettings(Mockito.any())).thenReturn(featureListMock); + try { + propertySource.initProperties(); + } catch (Exception e) { + assertEquals("Found Feature Flag /foo/test_key_1 with invalid Content Type of ", e.getMessage()); + } + } + + @Test + public void initNullInvalidContentTypeFeatureFlagTest() { + ArrayList items = new ArrayList<>(); + items.add(FEATURE_ITEM_NULL); + when(featureListMock.iterator()).thenReturn(Collections.emptyIterator()) + .thenReturn(items.iterator()); + when(clientMock.listSettings(Mockito.any())) + .thenReturn(featureListMock).thenReturn(featureListMock); + + propertySource.initProperties(); + + String[] keyNames = propertySource.getPropertyNames(); + String[] expectedKeyNames = {}; + + assertThat(keyNames).containsExactlyInAnyOrder(expectedKeyNames); + } + + @Test + public void testFeatureFlagTargeting() { + when(featureListMock.iterator()).thenReturn(FEATURE_ITEMS_TARGETING.iterator()); + when(clientMock.listSettings(Mockito.any())) + .thenReturn(featureListMock).thenReturn(featureListMock); + featureFlagStore.setEnabled(true); + + propertySource.initProperties(); + + FeatureSet featureSetExpected = new FeatureSet(); + Feature feature = new Feature(); + feature.setKey("target"); + HashMap filters = new HashMap<>(); + FeatureFlagFilter ffec = new FeatureFlagFilter("targetingFilter"); + + LinkedHashMap parameters = new LinkedHashMap<>(); + + LinkedHashMap users = new LinkedHashMap<>(); + users.put("0", "Jeff"); + users.put("1", "Alicia"); + + LinkedHashMap groups = new LinkedHashMap<>(); + LinkedHashMap ring0 = new LinkedHashMap<>(); + LinkedHashMap ring1 = new LinkedHashMap<>(); + + ring0.put("name", "Ring0"); + ring0.put("rolloutPercentage", "100"); + + ring1.put("name", "Ring1"); + ring1.put("rolloutPercentage", "100"); + + groups.put("0", ring0); + groups.put("1", ring1); + + parameters.put(USERS, users); + parameters.put(GROUPS, groups); + parameters.put(DEFAULT_ROLLOUT_PERCENTAGE, 50); + + ffec.setParameters(parameters); + filters.put(0, ffec); + feature.setEnabledFor(filters); + + featureSetExpected.addFeature("target", feature); + Feature targeting = (Feature) propertySource.getProperty(FEATURE_MANAGEMENT_KEY + "target"); + + FeatureFlagFilter filter = targeting.getEnabledFor().get(0); + + assertNotNull(filter); + assertEquals("targetingFilter", filter.getName()); + assertEquals(parameters.size(), filter.getParameters().size()); + } +} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java similarity index 52% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java index e467bf97b34d0..a8e1b1fcad160 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceKeyVaultTest.java @@ -2,27 +2,29 @@ // Licensed under the MIT License. package com.azure.spring.cloud.config.implementation; -import static com.azure.spring.cloud.config.AppConfigurationConstants.KEY_VAULT_CONTENT_TYPE; -import static com.azure.spring.cloud.config.TestConstants.TEST_CONN_STRING; -import static com.azure.spring.cloud.config.TestConstants.TEST_KEY_1; -import static com.azure.spring.cloud.config.TestConstants.TEST_KEY_2; -import static com.azure.spring.cloud.config.TestConstants.TEST_KEY_3; -import static com.azure.spring.cloud.config.TestConstants.TEST_KEY_VAULT_1; -import static com.azure.spring.cloud.config.TestConstants.TEST_LABEL_1; -import static com.azure.spring.cloud.config.TestConstants.TEST_LABEL_2; -import static com.azure.spring.cloud.config.TestConstants.TEST_LABEL_3; -import static com.azure.spring.cloud.config.TestConstants.TEST_LABEL_VAULT_1; -import static com.azure.spring.cloud.config.TestConstants.TEST_STORE_NAME; -import static com.azure.spring.cloud.config.TestConstants.TEST_URI_VAULT_1; -import static com.azure.spring.cloud.config.TestConstants.TEST_VALUE_1; -import static com.azure.spring.cloud.config.TestConstants.TEST_VALUE_2; -import static com.azure.spring.cloud.config.TestConstants.TEST_VALUE_3; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.KEY_VAULT_CONTENT_TYPE; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_CONN_STRING; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_KEY_1; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_KEY_2; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_KEY_3; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_KEY_VAULT_1; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_LABEL_1; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_LABEL_2; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_LABEL_3; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_LABEL_VAULT_1; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_STORE_NAME; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_URI_VAULT_1; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_VALUE_1; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_VALUE_2; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_VALUE_3; import static com.azure.spring.cloud.config.implementation.TestUtils.createItem; import static com.azure.spring.cloud.config.implementation.TestUtils.createSecretReference; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.when; import java.io.IOException; +import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -39,15 +41,8 @@ import com.azure.security.keyvault.secrets.SecretAsyncClient; import com.azure.security.keyvault.secrets.SecretClientBuilder; import com.azure.security.keyvault.secrets.models.KeyVaultSecret; -import com.azure.spring.cloud.config.KeyVaultCredentialProvider; -import com.azure.spring.cloud.config.KeyVaultSecretProvider; -import com.azure.spring.cloud.config.feature.management.entity.FeatureSet; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationProviderProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreSelects; -import com.azure.spring.cloud.config.properties.ConfigStore; - -import reactor.core.publisher.Mono; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.config.implementation.stores.AppConfigurationSecretClientManager; public class AppConfigurationPropertySourceKeyVaultTest { @@ -72,14 +67,16 @@ public class AppConfigurationPropertySourceKeyVaultTest { TEST_KEY_VAULT_1, TEST_URI_VAULT_1, TEST_LABEL_VAULT_1, KEY_VAULT_CONTENT_TYPE); - private AppConfigurationPropertySource propertySource; + private AppConfigurationApplicationSettingPropertySource propertySource; - private AppConfigurationProperties appConfigurationProperties; + @Mock + private SecretClientBuilder builderMock; - private AppConfigurationProviderProperties appProperties; + @Mock + private AppConfigurationKeyVaultClientFactory keyVaultClientFactory; @Mock - private SecretClientBuilder builderMock; + private AppConfigurationSecretClientManager clientManagerMock; @Mock private AppConfigurationReplicaClient replicaClientMock; @@ -88,9 +85,7 @@ public class AppConfigurationPropertySourceKeyVaultTest { private SecretAsyncClient clientMock; @Mock - private List configurationListMock; - - private KeyVaultCredentialProvider tokenCredentialProvider = null; + private List keyVaultSecretListMock; @BeforeEach public void init() { @@ -99,16 +94,10 @@ public void init() { KEY_VAULT_ITEM.setContentType(KEY_VAULT_CONTENT_TYPE); MockitoAnnotations.openMocks(this); - appConfigurationProperties = new AppConfigurationProperties(); - appProperties = new AppConfigurationProviderProperties(); - appProperties.setMaxRetryTime(0); - ConfigStore testStore = new ConfigStore(); - testStore.setEndpoint(TEST_STORE_NAME); - AppConfigurationStoreSelects selects = new AppConfigurationStoreSelects().setKeyFilter(KEY_FILTER) - .setLabelFilter("\0"); - propertySource = new AppConfigurationPropertySource(testStore, selects, new ArrayList<>(), - appConfigurationProperties, replicaClientMock, appProperties, tokenCredentialProvider, null, - new TestClient()); + + String[] labelFilter = { "\0" }; + propertySource = new AppConfigurationApplicationSettingPropertySource(TEST_STORE_NAME, replicaClientMock, + keyVaultClientFactory, KEY_FILTER, labelFilter, 60); TEST_ITEMS.add(ITEM_1); TEST_ITEMS.add(ITEM_2); @@ -121,20 +110,24 @@ public void cleanup() throws Exception { } @Test - public void testKeyVaultTest() throws AppConfigurationStatusException, IOException { + public void testKeyVaultTest() { TEST_ITEMS.add(KEY_VAULT_ITEM); - when(configurationListMock.iterator()).thenReturn(TEST_ITEMS.iterator()) + when(keyVaultSecretListMock.iterator()).thenReturn(TEST_ITEMS.iterator()) .thenReturn(Collections.emptyIterator()); - when(replicaClientMock.listConfigurationSettings(Mockito.any())).thenReturn(configurationListMock).thenReturn(configurationListMock); + when(replicaClientMock.listSettings(Mockito.any())).thenReturn(keyVaultSecretListMock) + .thenReturn(keyVaultSecretListMock); Mockito.when(builderMock.buildAsyncClient()).thenReturn(clientMock); KeyVaultSecret secret = new KeyVaultSecret("mySecret", "mySecretValue"); - when(clientMock.getSecret(Mockito.anyString(), Mockito.anyString())).thenReturn(Mono.just(secret)); + when(keyVaultClientFactory.getClient(Mockito.eq("https://test.key.vault.com"))).thenReturn(clientManagerMock); + when(clientManagerMock.getSecret(Mockito.any(URI.class), Mockito.anyInt())).thenReturn(secret); - FeatureSet featureSet = new FeatureSet(); - - propertySource.initProperties(featureSet); + try { + propertySource.initProperties(); + } catch (IOException e) { + fail("Failed Reading in Feature Flags"); + } String[] keyNames = propertySource.getPropertyNames(); String[] expectedKeyNames = TEST_ITEMS.stream() @@ -147,13 +140,4 @@ public void testKeyVaultTest() throws AppConfigurationStatusException, IOExcepti assertThat(propertySource.getProperty(TEST_KEY_3)).isEqualTo(TEST_VALUE_3); assertThat(propertySource.getProperty(TEST_KEY_VAULT_1)).isEqualTo("mySecretValue"); } - - class TestClient implements KeyVaultSecretProvider { - - @Override - public String getSecret(String uri) { - return "mySecretValue"; - } - - } } diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocatorTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocatorTest.java similarity index 60% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocatorTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocatorTest.java index 402c8f7a0769f..77459dee3dd4d 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocatorTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPropertySourceLocatorTest.java @@ -2,11 +2,12 @@ // Licensed under the MIT License. package com.azure.spring.cloud.config.implementation; -import static com.azure.spring.cloud.config.TestConstants.TEST_CONN_STRING; -import static com.azure.spring.cloud.config.TestConstants.TEST_CONN_STRING_2; -import static com.azure.spring.cloud.config.TestConstants.TEST_STORE_NAME; -import static com.azure.spring.cloud.config.TestConstants.TEST_STORE_NAME_1; -import static com.azure.spring.cloud.config.TestConstants.TEST_STORE_NAME_2; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.EMPTY_LABEL; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_CONN_STRING; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_CONN_STRING_2; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_STORE_NAME; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_STORE_NAME_1; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_STORE_NAME_2; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -26,6 +27,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; @@ -42,14 +44,14 @@ import com.azure.core.http.rest.PagedResponse; import com.azure.data.appconfiguration.ConfigurationAsyncClient; import com.azure.data.appconfiguration.models.ConfigurationSetting; -import com.azure.spring.cloud.config.KeyVaultCredentialProvider; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationProviderProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreMonitoring; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreSelects; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreTrigger; -import com.azure.spring.cloud.config.properties.ConfigStore; -import com.azure.spring.cloud.config.properties.FeatureFlagStore; +import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationKeyValueSelector; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProviderProperties; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreMonitoring; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreTrigger; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; +import com.azure.spring.cloud.config.implementation.properties.FeatureFlagStore; import reactor.core.publisher.Flux; @@ -74,6 +76,9 @@ public class AppConfigurationPropertySourceLocatorTest { @Mock private AppConfigurationReplicaClientFactory clientFactoryMock; + @Mock + private AppConfigurationKeyVaultClientFactory keyVaultClientFactory; + @Mock private AppConfigurationReplicaClient replicaClientMock; @@ -110,13 +115,13 @@ public class AppConfigurationPropertySourceLocatorTest { private AppConfigurationProperties properties; @Mock - private List configurationListMock; + private List watchKeyListMock; private AppConfigurationPropertySourceLocator locator; private AppConfigurationProviderProperties appProperties; - private KeyVaultCredentialProvider tokenCredentialProvider = null; + private List stores; @BeforeEach public void setup() { @@ -146,15 +151,13 @@ public Object getProperty(String name) { when(configStoreMock.getEndpoint()).thenReturn(TEST_STORE_NAME); when(configStoreMock.isEnabled()).thenReturn(true); - List stores = new ArrayList<>(); + stores = new ArrayList<>(); stores.add(configStoreMock); - properties.setStores(stores); - AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); monitoring.setEnabled(false); AppConfigurationStoreTrigger trigger = new AppConfigurationStoreTrigger(); - trigger.setKey("sentinal"); + trigger.setKey("sentinel"); trigger.setKey("test"); ArrayList triggers = new ArrayList<>(); triggers.add(trigger); @@ -162,23 +165,18 @@ public Object getProperty(String name) { when(configStoreMock.getMonitoring()).thenReturn(monitoring); when(configClientMock.listConfigurationSettings(Mockito.any())).thenReturn(settingsMock); - when(settingsMock.byPage()).thenReturn(pageMock); + when(iterableMock.iterator()).thenReturn(iteratorMock); when(iteratorMock.hasNext()).thenReturn(true).thenReturn(false); when(iteratorMock.next()).thenReturn(pagedMock); - when(pagedMock.getItems()).thenReturn(new ArrayList()); - when(configurationListMock.iterator()).thenReturn(Collections.emptyIterator()); + when(watchKeyListMock.iterator()).thenReturn(Collections.emptyIterator()); when(clientFactoryMock.getAvailableClients(Mockito.anyString(), Mockito.eq(true))) .thenReturn(Arrays.asList(replicaClientMock)); - when(replicaClientMock.listConfigurationSettings(Mockito.any())).thenReturn(configurationListMock) - .thenReturn(configurationListMock).thenReturn(configurationListMock); + when(replicaClientMock.listSettings(Mockito.any())).thenReturn(watchKeyListMock) + .thenReturn(watchKeyListMock).thenReturn(watchKeyListMock); when(replicaClientMock.getEndpoint()).thenReturn(TEST_STORE_NAME); - - - when(appPropertiesMock.getDefaultMinBackoff()).thenReturn((long) 30); - when(appPropertiesMock.getDefaultMaxBackoff()).thenReturn((long) 600); appProperties = new AppConfigurationProviderProperties(); appProperties.setVersion("1.0"); @@ -187,8 +185,8 @@ public Object getProperty(String name) { appProperties.setDefaultMaxBackoff((long) 600); appProperties.setDefaultMinBackoff((long) 30); - AppConfigurationStoreSelects selectedKeys = new AppConfigurationStoreSelects().setKeyFilter(KEY_FILTER); - List selects = new ArrayList<>(); + AppConfigurationKeyValueSelector selectedKeys = new AppConfigurationKeyValueSelector().setKeyFilter(KEY_FILTER); + List selects = new ArrayList<>(); selects.add(selectedKeys); when(configStoreMock.getSelects()).thenReturn(selects); } @@ -203,8 +201,8 @@ public void cleanup() throws Exception { public void compositeSourceIsCreated() { when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStoreMock); - locator = new AppConfigurationPropertySourceLocator(properties, appProperties, clientFactoryMock, - tokenCredentialProvider, null, null); + locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, + keyVaultClientFactory, null, stores); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -218,7 +216,84 @@ public void compositeSourceIsCreated() { KEY_FILTER + "store1/\0" }; assertEquals(expectedSourceNames.length, sources.size()); - assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(s -> s.getName()).toArray()); + assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); + } + } + + @Test + public void compositeSourceIsCreatedWithMonitoring() { + String watchKey = "wk1"; + String watchValue = "0"; + String watchLabel = EMPTY_LABEL; + AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); + monitoring.setEnabled(true); + List watchKeys = new ArrayList<>(); + AppConfigurationStoreTrigger trigger = new AppConfigurationStoreTrigger(); + trigger.setKey(watchKey); + trigger.setLabel(watchLabel); + watchKeys.add(trigger); + monitoring.setTriggers(watchKeys); + + when(configStoreMock.getMonitoring()).thenReturn(monitoring); + when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStoreMock); + when(replicaClientMock.getWatchKey(Mockito.eq(watchKey), Mockito.anyString())) + .thenReturn(TestUtils.createItem("", watchKey, watchValue, watchLabel, "")); + + locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, + null, stores); + + try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { + stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); + PropertySource source = locator.locate(emptyEnvironment); + + assertTrue(source instanceof CompositePropertySource); + + Collection> sources = ((CompositePropertySource) source).getPropertySources(); + + String[] expectedSourceNames = new String[] { + KEY_FILTER + "store1/\0" + }; + assertEquals(expectedSourceNames.length, sources.size()); + assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); + verify(replicaClientMock, times(1)).getWatchKey(Mockito.eq(watchKey), Mockito.anyString()); + } + } + + @Test + public void compositeSourceIsCreatedWithMonitoringNoWatchKey() { + String watchKey = "wk1"; + String watchLabel = EMPTY_LABEL; + AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); + monitoring.setEnabled(true); + List watchKeys = new ArrayList<>(); + AppConfigurationStoreTrigger trigger = new AppConfigurationStoreTrigger(); + trigger.setKey(watchKey); + trigger.setLabel(watchLabel); + watchKeys.add(trigger); + monitoring.setTriggers(watchKeys); + + when(configStoreMock.getMonitoring()).thenReturn(monitoring); + when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStoreMock); + when(replicaClientMock.getWatchKey(Mockito.eq(watchKey), Mockito.anyString())) + .thenReturn(null); + + locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, + null, stores); + + try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { + stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); + PropertySource source = locator.locate(emptyEnvironment); + + assertTrue(source instanceof CompositePropertySource); + + Collection> sources = ((CompositePropertySource) source).getPropertySources(); + + String[] expectedSourceNames = new String[] { + KEY_FILTER + "store1/\0" + }; + assertEquals(expectedSourceNames.length, sources.size()); + assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); + verify(replicaClientMock, times(1)).getWatchKey(Mockito.eq(watchKey), Mockito.anyString()); } } @@ -226,8 +301,8 @@ public void compositeSourceIsCreated() { public void devSourceIsCreated() { when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStoreMock); - locator = new AppConfigurationPropertySourceLocator(properties, appProperties, clientFactoryMock, - tokenCredentialProvider, null, null); + locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, + null, stores); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -240,7 +315,7 @@ public void devSourceIsCreated() { KEY_FILTER + "store1/dev" }; assertEquals(expectedSourceNames.length, sources.size()); - assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(s -> s.getName()).toArray()); + assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); } } @@ -248,8 +323,8 @@ public void devSourceIsCreated() { public void multiSourceIsCreated() { when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStoreMock); - locator = new AppConfigurationPropertySourceLocator(properties, appProperties, clientFactoryMock, - tokenCredentialProvider, null, null); + locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, + null, stores); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -262,7 +337,7 @@ public void multiSourceIsCreated() { KEY_FILTER + "store1/prod,dev" }; assertEquals(expectedSourceNames.length, sources.size()); - assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(s -> s.getName()).toArray()); + assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); } } @@ -270,11 +345,12 @@ public void multiSourceIsCreated() { public void storeCreatedWithFeatureFlags() { FeatureFlagStore featureFlagStore = new FeatureFlagStore(); featureFlagStore.setEnabled(true); + featureFlagStore.validateAndInit(); when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStore); - locator = new AppConfigurationPropertySourceLocator(properties, appProperties, clientFactoryMock, - tokenCredentialProvider, null, null); + locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, + null, stores); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -287,10 +363,49 @@ public void storeCreatedWithFeatureFlags() { // [/foo_prod/, /foo_dev/, /foo/, /application_prod/, /application_dev/, // /application/] String[] expectedSourceNames = new String[] { - KEY_FILTER + "store1/\0" + KEY_FILTER + "store1/\0", + "FM_store1/" + }; + assertEquals(expectedSourceNames.length, sources.size()); + assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); + } + } + + @Test + public void storeCreatedWithFeatureFlagsWithMonitoring() { + AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); + monitoring.setEnabled(true); + FeatureFlagStore featureFlagStore = new FeatureFlagStore(); + featureFlagStore.setEnabled(true); + featureFlagStore.validateAndInit(); + + List featureList = new ArrayList<>(); + FeatureFlagConfigurationSetting featureFlag = new FeatureFlagConfigurationSetting("Alpha", false); + featureList.add(featureFlag); + + when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStore); + when(configStoreMock.getMonitoring()).thenReturn(monitoring); + when(replicaClientMock.listSettings(Mockito.any())).thenReturn(featureList); + + locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, + null, stores); + + try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { + stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); + PropertySource source = locator.locate(emptyEnvironment); + assertTrue(source instanceof CompositePropertySource); + + Collection> sources = ((CompositePropertySource) source).getPropertySources(); + // Application name: foo and active profile: dev,prod, should construct below + // composite Property Source: + // [/foo_prod/, /foo_dev/, /foo/, /application_prod/, /application_dev/, + // /application/] + String[] expectedSourceNames = new String[] { + KEY_FILTER + "store1/\0", + "FM_store1/" }; assertEquals(expectedSourceNames.length, sources.size()); - assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(s -> s.getName()).toArray()); + assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); } } @@ -298,8 +413,8 @@ public void storeCreatedWithFeatureFlags() { public void watchedKeyCheck() { when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStoreMock); - locator = new AppConfigurationPropertySourceLocator(properties, appProperties, clientFactoryMock, - tokenCredentialProvider, null, null); + locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, + null, stores); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -315,17 +430,25 @@ public void watchedKeyCheck() { KEY_FILTER + "store1/\0" }; assertEquals(expectedSourceNames.length, sources.size()); - assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(s -> s.getName()).toArray()); + assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); } } @Test public void defaultFailFastThrowException() { + AppConfigurationStoreTrigger trigger = new AppConfigurationStoreTrigger(); + List triggers = new ArrayList<>(); + triggers.add(trigger); + AppConfigurationStoreMonitoring monitor = new AppConfigurationStoreMonitoring(); + monitor.setEnabled(true); + monitor.setTriggers(triggers); + when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStoreMock); when(configStoreMock.isFailFast()).thenReturn(true); + when(configStoreMock.getMonitoring()).thenReturn(monitor); - locator = new AppConfigurationPropertySourceLocator(properties, appProperties, - clientFactoryMock, tokenCredentialProvider, null, null); + locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, + null, stores); when(clientFactoryMock.getAvailableClients(Mockito.anyString())).thenReturn(Arrays.asList(replicaClientMock)); when(replicaClientMock.getWatchKey(Mockito.any(), Mockito.anyString())).thenThrow(new RuntimeException()); @@ -339,12 +462,12 @@ public void refreshThrowException() throws IllegalArgumentException { when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStoreMock); AppConfigurationPropertySourceLocator.STARTUP.set(false); - locator = new AppConfigurationPropertySourceLocator(properties, appProperties, - clientFactoryMock, tokenCredentialProvider, null, null); + locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, + null, stores); when(clientFactoryMock.getAvailableClients(Mockito.anyString())).thenReturn(Arrays.asList(replicaClientMock)); when(replicaClientMock.getWatchKey(Mockito.any(), Mockito.anyString())).thenThrow(new RuntimeException()); - when(replicaClientMock.listConfigurationSettings(any())).thenThrow(new RuntimeException()); + when(replicaClientMock.listSettings(any())).thenThrow(new RuntimeException()); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getLoadState(Mockito.anyString())).thenReturn(true); @@ -354,10 +477,11 @@ public void refreshThrowException() throws IllegalArgumentException { } @Test + @Disabled public void notFailFastShouldPass() { when(configStoreMock.isFailFast()).thenReturn(false); - locator = new AppConfigurationPropertySourceLocator(properties, appProperties, - clientFactoryMock, tokenCredentialProvider, null, null); + locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, + null, stores); when(configStoreMock.isFailFast()).thenReturn(false); when(configStoreMock.getEndpoint()).thenReturn(TEST_STORE_NAME); @@ -380,8 +504,8 @@ public void multiplePropertySourcesExistForMultiStores() { TestUtils.addStore(properties, TEST_STORE_NAME_1, TEST_CONN_STRING, KEY_FILTER); TestUtils.addStore(properties, TEST_STORE_NAME_2, TEST_CONN_STRING_2, KEY_FILTER); - locator = new AppConfigurationPropertySourceLocator(properties, appProperties, - clientFactoryMock, tokenCredentialProvider, null, null); + locator = new AppConfigurationPropertySourceLocator(appProperties, + clientFactoryMock, keyVaultClientFactory, null, properties.getStores()); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -392,7 +516,7 @@ public void multiplePropertySourcesExistForMultiStores() { String[] expectedSourceNames = new String[] { KEY_FILTER + TEST_STORE_NAME_2 + "/\0", KEY_FILTER + TEST_STORE_NAME_1 + "/\0" }; assertEquals(2, sources.size()); - assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(s -> s.getName()).toArray()); + assertArrayEquals((Object[]) expectedSourceNames, sources.stream().map(PropertySource::getName).toArray()); } } @@ -400,8 +524,6 @@ public void multiplePropertySourcesExistForMultiStores() { public void awaitOnError() { List configStores = new ArrayList<>(); configStores.add(configStoreMockError); - AppConfigurationProperties properties = new AppConfigurationProperties(); - properties.setStores(configStores); when(appPropertiesMock.getPrekillTime()).thenReturn(5); @@ -420,8 +542,9 @@ public Object getProperty(String name) { String[] array = {}; when(env.getActiveProfiles()).thenReturn(array); - AppConfigurationStoreSelects selectedKeys = new AppConfigurationStoreSelects().setKeyFilter("/application/"); - List selects = new ArrayList<>(); + AppConfigurationKeyValueSelector selectedKeys = new AppConfigurationKeyValueSelector() + .setKeyFilter("/application/"); + List selects = new ArrayList<>(); selects.add(selectedKeys); when(configStoreMockError.getSelects()).thenReturn(selects); when(configStoreMockError.isEnabled()).thenReturn(true); @@ -431,12 +554,12 @@ public Object getProperty(String name) { when(configStoreMockError.getFeatureFlags()).thenReturn(featureFlagStoreMock); when(clientFactoryMock.getAvailableClients(Mockito.anyString())).thenReturn(Arrays.asList(replicaClientMock)); - when(replicaClientMock.listConfigurationSettings(Mockito.any())).thenThrow(new NullPointerException("")); + when(replicaClientMock.listSettings(Mockito.any())).thenThrow(new NullPointerException("")); when(appPropertiesMock.getPrekillTime()).thenReturn(-60); when(appPropertiesMock.getStartDate()).thenReturn(Instant.now()); - locator = new AppConfigurationPropertySourceLocator(properties, appPropertiesMock, clientFactoryMock, - tokenCredentialProvider, null, null); + locator = new AppConfigurationPropertySourceLocator(appPropertiesMock, clientFactoryMock, keyVaultClientFactory, + null, configStores); assertThrows(RuntimeException.class, () -> locator.locate(env)); verify(appPropertiesMock, times(1)).getPrekillTime(); @@ -446,8 +569,8 @@ public Object getProperty(String name) { public void storeDisabled() { when(configStoreMock.isEnabled()).thenReturn(false); - locator = new AppConfigurationPropertySourceLocator(properties, appProperties, clientFactoryMock, - tokenCredentialProvider, null, null); + locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, + null, stores); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); PropertySource source = locator.locate(emptyEnvironment); diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefreshTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefreshTest.java similarity index 86% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefreshTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefreshTest.java index 67d0ea174f4ff..7690c8afe1aab 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefreshTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefreshTest.java @@ -27,7 +27,7 @@ public class AppConfigurationPullRefreshTest { @Mock private ApplicationEventPublisher publisher; - private Duration refreshInterval = Duration.ofMinutes(10); + private final Duration refreshInterval = Duration.ofMinutes(10); private RefreshEventData eventData; @@ -37,7 +37,6 @@ public class AppConfigurationPullRefreshTest { @BeforeEach public void setup() { MockitoAnnotations.openMocks(this); - eventData = new RefreshEventData(); } @@ -52,11 +51,11 @@ public void refreshNoChange() throws InterruptedException, ExecutionException { .mockStatic(AppConfigurationRefreshUtil.class)) { refreshUtils .when(() -> AppConfigurationRefreshUtil.refreshStoresCheck(Mockito.eq(clientFactoryMock), - Mockito.eq(refreshInterval), Mockito.any())) + Mockito.eq(refreshInterval), Mockito.any(), Mockito.any())) .thenReturn(eventData); - AppConfigurationPullRefresh refresh = new AppConfigurationPullRefresh(clientFactoryMock, - refreshInterval, (long) 0); + AppConfigurationPullRefresh refresh = new AppConfigurationPullRefresh(clientFactoryMock, refreshInterval, + (long) 0); assertFalse(refresh.refreshConfigurations().get()); } } @@ -68,11 +67,11 @@ public void refreshUpdate() throws InterruptedException, ExecutionException { .mockStatic(AppConfigurationRefreshUtil.class)) { refreshUtils .when(() -> AppConfigurationRefreshUtil.refreshStoresCheck(Mockito.eq(clientFactoryMock), - Mockito.eq(refreshInterval), Mockito.any())) + Mockito.eq(refreshInterval), Mockito.any(), Mockito.any())) .thenReturn(eventData); - AppConfigurationPullRefresh refresh = new AppConfigurationPullRefresh(clientFactoryMock, - refreshInterval, (long) 0); + AppConfigurationPullRefresh refresh = new AppConfigurationPullRefresh(clientFactoryMock, refreshInterval, + (long) 0); refresh.setApplicationEventPublisher(publisher); assertTrue(refresh.refreshConfigurations().get()); } diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtilTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtilTest.java similarity index 81% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtilTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtilTest.java index edba39e65004c..02d8d0eff174e 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtilTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtilTest.java @@ -2,7 +2,9 @@ // Licensed under the MIT License. package com.azure.spring.cloud.config.implementation; -import static com.azure.spring.cloud.config.AppConfigurationConstants.EMPTY_LABEL; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.EMPTY_LABEL; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.FEATURE_FLAG_PREFIX; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.times; @@ -10,7 +12,9 @@ import static org.mockito.Mockito.when; import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -27,8 +31,10 @@ import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; import com.azure.spring.cloud.config.implementation.AppConfigurationRefreshUtil.RefreshEventData; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreMonitoring; -import com.azure.spring.cloud.config.properties.FeatureFlagStore; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreMonitoring; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; +import com.azure.spring.cloud.config.implementation.properties.FeatureFlagKeyValueSelector; +import com.azure.spring.cloud.config.implementation.properties.FeatureFlagStore; public class AppConfigurationRefreshUtilTest { @@ -41,7 +47,7 @@ public class AppConfigurationRefreshUtilTest { private AppConfigurationReplicaClientFactory clientFactoryMock; @Mock - private List configurationListMock; + private List watchKeyListMock; @Mock private AppConfigurationReplicaClient clientOriginMock; @@ -52,26 +58,36 @@ public class AppConfigurationRefreshUtilTest { @Mock private ConnectionManager connectionManagerMock; + private ConfigStore configStore; + private String endpoint; private RefreshEventData eventData = new RefreshEventData(); - private List clients = new ArrayList<>(); + private final List clients = new ArrayList<>(); - private List watchKeys = generateWatchKeys(); + private final List watchKeys = generateWatchKeys(); - private List watchKeysFeatureFlags = generateFeatureFlagWatchKeys(); + private final List watchKeysFeatureFlags = generateFeatureFlagWatchKeys(); - private AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); + private final AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); - private FeatureFlagStore featureStore = new FeatureFlagStore(); + private final FeatureFlagStore featureStore = new FeatureFlagStore(); @BeforeEach public void setup() { MockitoAnnotations.openMocks(this); + configStore = new ConfigStore(); featureStore.setEnabled(true); + List ffSelects = new ArrayList<>(); + FeatureFlagKeyValueSelector ffSelect = new FeatureFlagKeyValueSelector().setKeyFilter(FEATURE_FLAG_PREFIX) + .setLabelFilter(EMPTY_LABEL); + ffSelects.add(ffSelect); + featureStore.setSelects(ffSelects); + configStore.setFeatureFlags(featureStore); + monitoring.setEnabled(true); featureStore.setEnabled(true); } @@ -86,7 +102,8 @@ public void refreshWithoutTimeWatchKeyConfigStoreNotLoaded(TestInfo testInfo) { stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(false); assertFalse( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore, + new ArrayList<>())); } } @@ -104,7 +121,9 @@ public void refreshWithoutTimeWatchKeyConfigStoreWatchKeyNotReturned(TestInfo te stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(newState); assertFalse( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore, + new ArrayList<>())); + } } @@ -118,14 +137,21 @@ public void refreshWithoutTimeWatchKeyConfigStoreWatchKeyNoChange(TestInfo testI State newState = new State(watchKeys, Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint); + List listedKeys = new ArrayList<>(); + listedKeys.add(watchKeysFeatureFlags.get(0)); + // Config Store does return a watch key change. when(clientMock.getWatchKey(Mockito.eq(KEY_FILTER), Mockito.eq(EMPTY_LABEL))).thenReturn(updatedWatchKey); + when(clientMock.listSettings(Mockito.any(SettingSelector.class))).thenReturn(watchKeyListMock); + when(watchKeyListMock.iterator()).thenReturn(listedKeys.iterator()); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { - stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(true); - stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(newState); + stateHolderMock.when(() -> StateHolder.getLoadStateFeatureFlag(endpoint)).thenReturn(true); + stateHolderMock.when(() -> StateHolder.getStateFeatureFlag(endpoint)).thenReturn(newState); assertFalse( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore, + new ArrayList<>())); + } } @@ -134,13 +160,15 @@ public void refreshWithoutTimeFeatureFlagDisabled(TestInfo testInfo) { endpoint = testInfo.getDisplayName() + ".azconfig.io"; when(clientMock.getEndpoint()).thenReturn(endpoint); - featureStore.setEnabled(false); + configStore.getFeatureFlags().setEnabled(false); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getLoadStateFeatureFlag(endpoint)).thenReturn(false); assertFalse( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore, + new ArrayList<>())); + } } @@ -149,13 +177,15 @@ public void refreshWithoutTimeFeatureFlagNotLoaded(TestInfo testInfo) { endpoint = testInfo.getDisplayName() + ".azconfig.io"; when(clientMock.getEndpoint()).thenReturn(endpoint); - featureStore.setEnabled(true); + configStore.getFeatureFlags().setEnabled(true); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getLoadStateFeatureFlag(endpoint)).thenReturn(false); assertFalse( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore, + new ArrayList<>())); + } } @@ -171,16 +201,17 @@ public void refreshWithoutTimeFeatureFlagNoChange(TestInfo testInfo) { listedKeys.add(watchKeysFeatureFlags.get(0)); // Config Store doesn't return a watch key change. - when(clientMock.listConfigurationSettings(Mockito.any(SettingSelector.class))) - .thenReturn(configurationListMock); - when(configurationListMock.iterator()).thenReturn(listedKeys.iterator()); + when(clientMock.listSettings(Mockito.any(SettingSelector.class))).thenReturn(watchKeyListMock); + when(watchKeyListMock.iterator()).thenReturn(listedKeys.iterator()); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getLoadStateFeatureFlag(endpoint)).thenReturn(true); stateHolderMock.when(() -> StateHolder.getStateFeatureFlag(endpoint)).thenReturn(newState); assertFalse( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore, + new ArrayList<>())); + } } @@ -200,15 +231,16 @@ public void refreshWithoutTimeFeatureFlagNoWatchKeyReturned(TestInfo testInfo) { endpoint); // Config Store does return a watch key change. - when(clientMock.listConfigurationSettings(Mockito.any(SettingSelector.class))) - .thenReturn(configurationListMock); - when(configurationListMock.iterator()).thenReturn(listedKeys.iterator()); + when(clientMock.listSettings(Mockito.any(SettingSelector.class))).thenReturn(watchKeyListMock); + when(watchKeyListMock.iterator()).thenReturn(listedKeys.iterator()); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getLoadStateFeatureFlag(endpoint)).thenReturn(true); stateHolderMock.when(() -> StateHolder.getStateFeatureFlag(endpoint)).thenReturn(newState); assertTrue( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore, + new ArrayList<>())); + } } @@ -218,9 +250,8 @@ public void refreshWithoutTimeFeatureFlagWasDeleted(TestInfo testInfo) { when(clientMock.getEndpoint()).thenReturn(endpoint); // Config Store doesn't return a value, Feature Flag was deleted - when(clientMock.listConfigurationSettings(Mockito.any(SettingSelector.class))) - .thenReturn(configurationListMock); - when(configurationListMock.iterator()).thenReturn(new ArrayList().iterator()); + when(clientMock.listSettings(Mockito.any(SettingSelector.class))).thenReturn(watchKeyListMock); + when(watchKeyListMock.iterator()).thenReturn(Collections.emptyIterator()); State newState = new State(watchKeysFeatureFlags, Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint); @@ -230,7 +261,9 @@ public void refreshWithoutTimeFeatureFlagWasDeleted(TestInfo testInfo) { stateHolderMock.when(() -> StateHolder.getStateFeatureFlag(endpoint)).thenReturn(newState); assertTrue( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore, + new ArrayList<>())); + } } @@ -248,15 +281,16 @@ public void refreshWithoutTimeFeatureFlagWasAdded(TestInfo testInfo) { endpoint); // Config Store returns a new feature flag - when(clientMock.listConfigurationSettings(Mockito.any(SettingSelector.class))) - .thenReturn(configurationListMock); - when(configurationListMock.iterator()).thenReturn(listedKeys.iterator()); + when(clientMock.listSettings(Mockito.any(SettingSelector.class))).thenReturn(watchKeyListMock); + when(watchKeyListMock.iterator()).thenReturn(listedKeys.iterator()); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getLoadStateFeatureFlag(endpoint)).thenReturn(true); stateHolderMock.when(() -> StateHolder.getStateFeatureFlag(endpoint)).thenReturn(newState); assertTrue( - AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore)); + AppConfigurationRefreshUtil.checkStoreAfterRefreshFailed(clientMock, clientFactoryMock, featureStore, + new ArrayList<>())); + } } @@ -284,8 +318,8 @@ public void refreshStoresCheckSettingsTestNotEnabled(TestInfo testInfo) { stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(newState); // Monitor is disabled - eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, - Duration.ofMinutes(10), (long) 0); + eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), + new ArrayList<>(), (long) 60); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -314,8 +348,8 @@ public void refreshStoresCheckSettingsTestNotLoaded(TestInfo testInfo) { stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(false); stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(newState); - eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, - Duration.ofMinutes(10), (long) 0); + eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), + new ArrayList<>(), (long) 60); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -345,8 +379,8 @@ public void refreshStoresCheckSettingsTestNotRefreshTime(TestInfo testInfo) { stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(true); stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(newState); - eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, - Duration.ofMinutes(10), (long) 0); + eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), + new ArrayList<>(), (long) 60); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -376,8 +410,8 @@ public void refreshStoresCheckSettingsTestFailedRequest(TestInfo testInfo) { stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(true); stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(newState); - eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, - Duration.ofMinutes(10), (long) 0); + eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), + new ArrayList<>(), (long) 60); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -409,8 +443,8 @@ public void refreshStoresCheckSettingsTestRefreshTimeNoChange(TestInfo testInfo) stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(true); stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(newState); - eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, - Duration.ofMinutes(10), (long) 0); + eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), + new ArrayList<>(), (long) 60); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -444,10 +478,10 @@ public void refreshStoresCheckSettingsTestTriggerRefresh(TestInfo testInfo) { try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(true); stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(newState); - stateHolderMock.when(() -> StateHolder.getCurrentState()).thenReturn(currentStateMock); + stateHolderMock.when(StateHolder::getCurrentState).thenReturn(currentStateMock); - eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, - Duration.ofMinutes(10), (long) 0); + eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), + new ArrayList<>(), (long) 60); assertTrue(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -480,8 +514,8 @@ public void refreshStoresCheckFeatureFlagTestNotLoaded(TestInfo testInfo) { stateHolderMock.when(() -> StateHolder.getStateFeatureFlag(endpoint)).thenReturn(newState); // Monitor is disabled - eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, - Duration.ofMinutes(10), (long) 0); + eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), + new ArrayList<>(), (long) 60); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -513,8 +547,8 @@ public void refreshStoresCheckFeatureFlagTestNotRefreshTime(TestInfo testInfo) { stateHolderMock.when(() -> StateHolder.getStateFeatureFlag(endpoint)).thenReturn(newState); // Monitor is disabled - eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, - Duration.ofMinutes(10), (long) 0); + eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), + new ArrayList<>(), (long) 60); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -527,7 +561,10 @@ public void refreshStoresCheckFeatureFlagTestNoChange(TestInfo testInfo) { when(clientMock.getEndpoint()).thenReturn(endpoint); clients.add(clientOriginMock); + configStore.setEndpoint(endpoint); + configStore.setFeatureFlags(featureStore); monitoring.setEnabled(false); + configStore.setMonitoring(monitoring); List listedKeys = new ArrayList<>(); listedKeys.add(watchKeysFeatureFlags.get(0)); @@ -540,8 +577,8 @@ public void refreshStoresCheckFeatureFlagTestNoChange(TestInfo testInfo) { when(clientFactoryMock.getConnections()).thenReturn(connections); when(clientFactoryMock.getAvailableClients(Mockito.eq(endpoint))).thenReturn(clients); - when(clientOriginMock.listConfigurationSettings(Mockito.any())).thenReturn(configurationListMock); - when(configurationListMock.iterator()).thenReturn(listedKeys.iterator()); + when(clientOriginMock.listSettings(Mockito.any())).thenReturn(watchKeyListMock); + when(watchKeyListMock.iterator()).thenReturn(listedKeys.iterator()); State newState = new State(generateFeatureFlagWatchKeys(), Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint); @@ -550,11 +587,11 @@ public void refreshStoresCheckFeatureFlagTestNoChange(TestInfo testInfo) { try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getLoadStateFeatureFlag(endpoint)).thenReturn(true); stateHolderMock.when(() -> StateHolder.getStateFeatureFlag(endpoint)).thenReturn(newState); - stateHolderMock.when(() -> StateHolder.getCurrentState()).thenReturn(currentStateMock); + stateHolderMock.when(StateHolder::getCurrentState).thenReturn(currentStateMock); // Monitor is disabled - eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, - Duration.ofMinutes(10), (long) 0); + eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), + new ArrayList<>(), (long) 60); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -585,8 +622,8 @@ public void refreshStoresCheckFeatureFlagTestTriggerRefresh(TestInfo testInfo) { listedKeys.add(updated); when(clientFactoryMock.getAvailableClients(Mockito.eq(endpoint))).thenReturn(clients); - when(clientOriginMock.listConfigurationSettings(Mockito.any())).thenReturn(configurationListMock); - when(configurationListMock.iterator()).thenReturn(listedKeys.iterator()); + when(clientOriginMock.listSettings(Mockito.any())).thenReturn(watchKeyListMock); + when(watchKeyListMock.iterator()).thenReturn(listedKeys.iterator()); State newState = new State(generateFeatureFlagWatchKeys(), Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint); @@ -595,11 +632,11 @@ public void refreshStoresCheckFeatureFlagTestTriggerRefresh(TestInfo testInfo) { try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getLoadStateFeatureFlag(endpoint)).thenReturn(true); stateHolderMock.when(() -> StateHolder.getStateFeatureFlag(endpoint)).thenReturn(newState); - stateHolderMock.when(() -> StateHolder.getCurrentState()).thenReturn(currentStateMock); + stateHolderMock.when(StateHolder::getCurrentState).thenReturn(currentStateMock); // Monitor is disabled - eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, - Duration.ofMinutes(10), (long) 0); + eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), + new ArrayList<>(), (long) 60); assertTrue(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -607,6 +644,17 @@ public void refreshStoresCheckFeatureFlagTestTriggerRefresh(TestInfo testInfo) { } } + @Test + public void minRefreshPeriodTest() { + try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { + stateHolderMock.when(() -> StateHolder.getNextForcedRefresh()).thenReturn(Instant.now().minusSeconds(600)); + RefreshEventData eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, + Duration.ofMinutes(1), new ArrayList(), (long) 0); + assertTrue(eventData.getDoRefresh()); + assertEquals("Minimum refresh period reached. Refreshing configurations.", eventData.getMessage()); + } + } + private List generateWatchKeys() { List watchKeys = new ArrayList<>(); diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientBuilderTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientBuilderTest.java similarity index 58% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientBuilderTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientBuilderTest.java index 1a04da3711522..66c6ba452d793 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientBuilderTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientBuilderTest.java @@ -2,10 +2,10 @@ // Licensed under the MIT License. package com.azure.spring.cloud.config.implementation; -import static com.azure.spring.cloud.config.TestConstants.TEST_CONN_STRING; -import static com.azure.spring.cloud.config.TestConstants.TEST_CONN_STRING_GEO; -import static com.azure.spring.cloud.config.TestConstants.TEST_ENDPOINT; -import static com.azure.spring.cloud.config.TestConstants.TEST_ENDPOINT_GEO; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_CONN_STRING; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_CONN_STRING_GEO; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_ENDPOINT; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_ENDPOINT_GEO; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -24,34 +24,34 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import org.springframework.core.env.Environment; import com.azure.core.credential.TokenCredential; import com.azure.data.appconfiguration.ConfigurationClientBuilder; -import com.azure.identity.ManagedIdentityCredential; -import com.azure.spring.cloud.config.AppConfigurationCredentialProvider; -import com.azure.spring.cloud.config.ConfigurationClientBuilderSetup; -import com.azure.spring.cloud.config.properties.AppConfigurationProviderProperties; -import com.azure.spring.cloud.config.properties.ConfigStore; +import com.azure.spring.cloud.config.ConfigurationClientCustomizer; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; +import com.azure.spring.cloud.service.implementation.appconfiguration.ConfigurationClientBuilderFactory; public class AppConfigurationReplicaClientBuilderTest { @Mock private ConfigurationClientBuilder builderMock; - @Mock - private AppConfigurationCredentialProvider tokenProviderMock; - @Mock private TokenCredential credentialMock; @Mock - private ConfigurationClientBuilderSetup modifierMock; + private ConfigurationClientCustomizer modifierMock; AppConfigurationReplicaClientsBuilder clientBuilder; private ConfigStore configStore; - - private AppConfigurationProviderProperties providerProperties; + + @Mock + private ConfigurationClientBuilderFactory clientFactoryMock; + + @Mock + private Environment envMock; @BeforeEach public void setup() { @@ -62,44 +62,20 @@ public void setup() { configStore.validateAndInit(); - providerProperties = new AppConfigurationProviderProperties(); - providerProperties.setDefaultMaxBackoff((long) 1000); - providerProperties.setDefaultMinBackoff((long) 1000); - clientBuilder = null; + when(envMock.getActiveProfiles()).thenReturn(new String[0]); + when(clientFactoryMock.build()).thenReturn(builderMock); } @Test public void buildClientFromEndpointTest() { - clientBuilder = new AppConfigurationReplicaClientsBuilder(0); - clientBuilder.setTokenCredentialProvider(tokenProviderMock); - AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); - Mockito.doReturn(builderMock).when(spy).getBuilder(); - - ConfigurationClientBuilder builder = new ConfigurationClientBuilder(); - when(builderMock.endpoint(Mockito.eq(TEST_ENDPOINT))).thenReturn(builder); - when(builderMock.addPolicy(Mockito.any())).thenReturn(builderMock); - - AppConfigurationReplicaClient replicaClient = spy.buildClients(configStore).get(0); - - assertNotNull(replicaClient); - assertTrue(replicaClient.getBackoffEndTime().isBefore(Instant.now().plusSeconds(1))); - assertEquals(TEST_ENDPOINT, replicaClient.getEndpoint()); - assertEquals(0, replicaClient.getFailedAttempts()); - } - - @Test - public void buildClientFromEndpointWithTokenCredentialTest() { - clientBuilder = new AppConfigurationReplicaClientsBuilder(0); - clientBuilder.setTokenCredentialProvider(tokenProviderMock); - + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); + clientBuilder.setEnvironment(envMock); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); - Mockito.doReturn(builderMock).when(spy).getBuilder(); ConfigurationClientBuilder builder = new ConfigurationClientBuilder(); when(builderMock.endpoint(Mockito.eq(TEST_ENDPOINT))).thenReturn(builder); when(builderMock.addPolicy(Mockito.any())).thenReturn(builderMock); - when(tokenProviderMock.getAppConfigCredential(Mockito.eq(TEST_ENDPOINT))).thenReturn(credentialMock); AppConfigurationReplicaClient replicaClient = spy.buildClients(configStore).get(0); @@ -107,32 +83,6 @@ public void buildClientFromEndpointWithTokenCredentialTest() { assertTrue(replicaClient.getBackoffEndTime().isBefore(Instant.now().plusSeconds(1))); assertEquals(TEST_ENDPOINT, replicaClient.getEndpoint()); assertEquals(0, replicaClient.getFailedAttempts()); - - verify(tokenProviderMock, times(1)).getAppConfigCredential(Mockito.anyString()); - verify(builderMock, times(1)).credential(Mockito.eq(credentialMock)); - } - - @Test - public void buildClientFromEndpointClientIdTest() { - clientBuilder = new AppConfigurationReplicaClientsBuilder(0); - clientBuilder.setTokenCredentialProvider(tokenProviderMock); - clientBuilder.setClientId("1234-5678-9012-3456"); - - AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); - Mockito.doReturn(builderMock).when(spy).getBuilder(); - - ConfigurationClientBuilder builder = new ConfigurationClientBuilder(); - when(builderMock.endpoint(Mockito.eq(TEST_ENDPOINT))).thenReturn(builder); - when(builderMock.addPolicy(Mockito.any())).thenReturn(builderMock); - - AppConfigurationReplicaClient replicaClient = spy.buildClients(configStore).get(0); - - assertNotNull(replicaClient); - assertTrue(replicaClient.getBackoffEndTime().isBefore(Instant.now().plusSeconds(1))); - assertEquals(TEST_ENDPOINT, replicaClient.getEndpoint()); - assertEquals(0, replicaClient.getFailedAttempts()); - - verify(builderMock, times(1)).credential(Mockito.any(ManagedIdentityCredential.class)); } @Test @@ -141,14 +91,12 @@ public void buildClientFromConnectionStringTest() { configStore.setConnectionString(TEST_CONN_STRING); configStore.validateAndInit(); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0); - clientBuilder.setTokenCredentialProvider(tokenProviderMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); + clientBuilder.setEnvironment(envMock); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); - Mockito.doReturn(builderMock).when(spy).getBuilder(); + + when(builderMock.connectionString(Mockito.anyString())).thenReturn(builderMock); - ConfigurationClientBuilder builder = new ConfigurationClientBuilder(); - when(builderMock.endpoint(Mockito.eq("test.endpoint"))).thenReturn(builder); - when(builderMock.addPolicy(Mockito.any())).thenReturn(builderMock); List clients = spy.buildClients(configStore); @@ -161,12 +109,11 @@ public void buildClientFromConnectionStringTest() { @Test public void modifyClientTest() { - clientBuilder = new AppConfigurationReplicaClientsBuilder(0); - clientBuilder.setTokenCredentialProvider(tokenProviderMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); clientBuilder.setClientProvider(modifierMock); + clientBuilder.setEnvironment(envMock); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); - Mockito.doReturn(builderMock).when(spy).getBuilder(); ConfigurationClientBuilder builder = new ConfigurationClientBuilder(); when(builderMock.endpoint(Mockito.eq(TEST_ENDPOINT))).thenReturn(builder); @@ -194,11 +141,10 @@ public void buildClientsFromMultipleEndpointsTest() { configStore.validateAndInit(); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0); - clientBuilder.setTokenCredentialProvider(tokenProviderMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); + clientBuilder.setEnvironment(envMock); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); - Mockito.doReturn(builderMock).when(spy).getBuilder(); ConfigurationClientBuilder builder = new ConfigurationClientBuilder(); when(builderMock.endpoint(Mockito.eq(TEST_ENDPOINT))).thenReturn(builder); @@ -210,7 +156,7 @@ public void buildClientsFromMultipleEndpointsTest() { } @Test - @Disabled("Disabled until connection string support is added.") + @Disabled // Waiting on Server Side Support for connection strings public void buildClientsFromMultipleConnectionStringsTest() { configStore = new ConfigStore(); List connectionStrings = new ArrayList<>(); @@ -222,11 +168,10 @@ public void buildClientsFromMultipleConnectionStringsTest() { configStore.validateAndInit(); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0); - clientBuilder.setTokenCredentialProvider(tokenProviderMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); + clientBuilder.setEnvironment(envMock); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); - Mockito.doReturn(builderMock).when(spy).getBuilder(); ConfigurationClientBuilder builder = new ConfigurationClientBuilder(); when(builderMock.endpoint(Mockito.eq(TEST_ENDPOINT))).thenReturn(builder); @@ -248,8 +193,8 @@ public void endpointAndConnectionString() { configStore.setConnectionString(TEST_CONN_STRING); configStore.validateAndInit(); - clientBuilder = new AppConfigurationReplicaClientsBuilder(0); - clientBuilder.setTokenCredentialProvider(tokenProviderMock); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); + clientBuilder.setEnvironment(envMock); String message = assertThrows(IllegalArgumentException.class, () -> clientBuilder.buildClients(configStore).get(0)).getMessage(); diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactoryTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactoryTest.java similarity index 72% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactoryTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactoryTest.java index 504b7c2d6e9bc..f8550e25ac92a 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactoryTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactoryTest.java @@ -7,7 +7,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -15,28 +14,22 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.properties.ConfigStore; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; public class AppConfigurationReplicaClientFactoryTest { private AppConfigurationReplicaClientFactory clientFactory; - @Mock - private ConnectionManager connectionManagerMock; - @Mock private AppConfigurationReplicaClientsBuilder clientBuilderMock; - private AppConfigurationProperties properties; - - private String originEndpoint = "clientfactorytest.azconfig.io"; + private final String originEndpoint = "clientFactoryTest.azconfig.io"; - private String replica1 = "clientfactorytest-replica1.azconfig.io"; + private final String replica1 = "clientFactoryTest-replica1.azconfig.io"; - private String noReplicaEndpoint = "noReplica.azconfig.io"; + private final String noReplicaEndpoint = "noReplica.azconfig.io"; - private String invalidReplica = "invalidreplica.azconfig.io"; + private final String invalidReplica = "invalidReplica.azconfig.io"; @BeforeEach public void setup() { @@ -57,13 +50,7 @@ public void setup() { storeNoReplica.setEndpoint(noReplicaEndpoint); stores.add(storeNoReplica); - properties = new AppConfigurationProperties(); - properties.setStores(stores); - - HashMap connections = new HashMap<>(); - connections.put(originEndpoint, connectionManagerMock); - - clientFactory = new AppConfigurationReplicaClientFactory(clientBuilderMock, properties); + clientFactory = new AppConfigurationReplicaClientFactory(clientBuilderMock, stores); } @Test diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientTest.java similarity index 65% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientTest.java index 7c2d58af0ce5b..04e5e3c861fe9 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientTest.java @@ -4,8 +4,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -26,14 +31,14 @@ public class AppConfigurationReplicaClientTest { @Mock private HttpResponseException exceptionMock; - + @Mock private HttpResponse responseMock; - + @Mock private PagedIterable settingsMock; - private String endpoint = "clienttest.azconfig.io"; + private final String endpoint = "clientTest.azconfig.io"; @BeforeEach public void setup() { @@ -55,40 +60,68 @@ public void getWatchKeyTest() { when(exceptionMock.getResponse()).thenReturn(responseMock); when(responseMock.getStatusCode()).thenReturn(429); assertThrows(AppConfigurationStatusException.class, () -> client.getWatchKey("watch", "\0")); - when(responseMock.getStatusCode()).thenReturn(408); assertThrows(AppConfigurationStatusException.class, () -> client.getWatchKey("watch", "\0")); - + when(responseMock.getStatusCode()).thenReturn(500); assertThrows(AppConfigurationStatusException.class, () -> client.getWatchKey("watch", "\0")); - + when(responseMock.getStatusCode()).thenReturn(499); assertThrows(HttpResponseException.class, () -> client.getWatchKey("watch", "\0")); } - + @Test public void listSettingsTest() { AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + List configurations = new ArrayList<>(); + when(clientMock.listConfigurationSettings(Mockito.any())).thenReturn(settingsMock); + when(settingsMock.iterator()).thenReturn(configurations.iterator()); - assertEquals(0, client.listConfigurationSettings(new SettingSelector()).size()); + assertEquals(configurations, client.listSettings(new SettingSelector())); when(clientMock.listConfigurationSettings(Mockito.any())).thenThrow(exceptionMock); when(exceptionMock.getResponse()).thenReturn(responseMock); when(responseMock.getStatusCode()).thenReturn(429); - assertThrows(AppConfigurationStatusException.class, () -> client.listConfigurationSettings(new SettingSelector())); - + assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector())); when(responseMock.getStatusCode()).thenReturn(408); - assertThrows(AppConfigurationStatusException.class, () -> client.listConfigurationSettings(new SettingSelector())); - + assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector())); + when(responseMock.getStatusCode()).thenReturn(500); - assertThrows(AppConfigurationStatusException.class, () -> client.listConfigurationSettings(new SettingSelector())); - + assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector())); + when(responseMock.getStatusCode()).thenReturn(499); - assertThrows(HttpResponseException.class, () -> client.listConfigurationSettings(new SettingSelector())); + assertThrows(HttpResponseException.class, () -> client.listSettings(new SettingSelector())); + } + + @Test + public void backoffTest() { + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, clientMock); + + // Setups in the past and with no errors. + assertTrue(client.getBackoffEndTime().isBefore(Instant.now())); + assertEquals(0, client.getFailedAttempts()); + + // Failing results in an increase in failed attempts + client.updateBackoffEndTime(Instant.now().plusSeconds(600)); + + assertTrue(client.getBackoffEndTime().isAfter(Instant.now())); + assertEquals(1, client.getFailedAttempts()); + + client.updateBackoffEndTime(Instant.now().minusSeconds(600)); + + assertTrue(client.getBackoffEndTime().isBefore(Instant.now())); + assertEquals(2, client.getFailedAttempts()); + + // Success in a list request results in a reset of failed attemtps + when(clientMock.listConfigurationSettings(Mockito.any(SettingSelector.class))).thenReturn(settingsMock); + + client.listSettings(new SettingSelector()); + assertTrue(client.getBackoffEndTime().isBefore(Instant.now())); + assertEquals(0, client.getFailedAttempts()); } } diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/BackoffTimeCalculatorTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/BackoffTimeCalculatorTest.java similarity index 99% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/BackoffTimeCalculatorTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/BackoffTimeCalculatorTest.java index d745975f29291..b745a31c40cd3 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/BackoffTimeCalculatorTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/BackoffTimeCalculatorTest.java @@ -35,5 +35,4 @@ public void testCalculate() { assertTrue(calculatedTime > testTime); } - } diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/ConnectionManagerTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/ConnectionManagerTest.java similarity index 77% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/ConnectionManagerTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/ConnectionManagerTest.java index df5c6711a7afd..06cd41619e9f6 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/ConnectionManagerTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/ConnectionManagerTest.java @@ -2,11 +2,11 @@ // Licensed under the MIT License. package com.azure.spring.cloud.config.implementation; -import static com.azure.spring.cloud.config.TestConstants.TEST_CONN_STRING; -import static com.azure.spring.cloud.config.TestConstants.TEST_CONN_STRING_GEO; -import static com.azure.spring.cloud.config.TestConstants.TEST_ENDPOINT; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_ENDPOINT; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.time.Instant; @@ -14,14 +14,13 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import com.azure.spring.cloud.config.health.AppConfigurationStoreHealth; -import com.azure.spring.cloud.config.properties.ConfigStore; +import com.azure.spring.cloud.config.implementation.health.AppConfigurationStoreHealth; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; public class ConnectionManagerTest { @@ -71,15 +70,14 @@ public void getStoreIdentifierTest() { } @Test - @Disabled("Disabled until connection string support is added.") public void backoffTest() { configStore = new ConfigStore(); - List connectionStrings = new ArrayList<>(); + List endpoints = new ArrayList<>(); + + endpoints.add("https://fake.test.config.io"); + endpoints.add("https://fake.test.geo.config.io"); - connectionStrings.add(TEST_CONN_STRING); - connectionStrings.add(TEST_CONN_STRING_GEO); - - //configStore.setConnectionStrings(connectionStrings); + configStore.setEndpoints(endpoints); configStore.validateAndInit(); @@ -94,8 +92,7 @@ public void backoffTest() { when(replicaClient2.getBackoffEndTime()).thenReturn(Instant.now().minusSeconds(60)); String originEndpoint = configStore.getEndpoint(); - String replicaEndpoint = AppConfigurationReplicaClientsBuilder - .getEndpointFromConnectionString(configStore.getConnectionStrings().get(1)); + String replicaEndpoint = endpoints.get(1); when(replicaClient1.getEndpoint()).thenReturn(originEndpoint); when(replicaClient2.getEndpoint()).thenReturn(replicaEndpoint); @@ -137,4 +134,23 @@ public void backoffTest() { assertTrue(connectionManager.getAllEndpoints().containsAll(expectedEndpoints)); assertEquals(AppConfigurationStoreHealth.DOWN, connectionManager.getHealth()); } + + @Test + public void updateSyncTokenTest() { + String fakeToken = "fakeToken"; + ConnectionManager manager = new ConnectionManager(clientBuilderMock, configStore); + + List clients = new ArrayList<>(); + clients.add(replicaClient1); + + when(clientBuilderMock.buildClients(Mockito.eq(configStore))).thenReturn(clients); + when(replicaClient1.getEndpoint()).thenReturn(TEST_ENDPOINT); + + List availableClients = manager.getAvailableClients(); + assertEquals(1, availableClients.size()); + + manager.updateSyncToken(TEST_ENDPOINT, fakeToken); + + verify(replicaClient1, times(1)).updateSyncToken(Mockito.eq(fakeToken)); + } } diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/JsonConfigurationParserTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/JsonConfigurationParserTest.java similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/JsonConfigurationParserTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/JsonConfigurationParserTest.java diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/StateHolderTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/StateHolderTest.java similarity index 92% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/StateHolderTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/StateHolderTest.java index f263a460cc74c..c8183ccba9d0f 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/StateHolderTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/StateHolderTest.java @@ -24,21 +24,19 @@ public class StateHolderTest { - private List watchKeys = new ArrayList<>(); + private final List watchKeys = new ArrayList<>(); @BeforeEach public void setup() { MockitoAnnotations.openMocks(this); - - ConfigurationSetting watchKey = new ConfigurationSetting().setKey("sentinal").setValue("0").setETag("current"); + ConfigurationSetting watchKey = new ConfigurationSetting().setKey("sentinel").setValue("0").setETag("current"); watchKeys.add(watchKey); - BackoffTimeCalculator.setDefaults((long) 600, (long) 30); } /** * Because of static code these need to run all at once. - * @param testInfo Test Names are used as static code key names + * @param testInfo */ @Test public void stateHolderTest(TestInfo testInfo) { @@ -81,10 +79,10 @@ private void stateExpiredTest(TestInfo testInfo) { StateHolder.updateState(expiredNegativeDurationStateHolder); - State originalExpireNagativeState = StateHolder.getState(endpoint); + State originalExpireNegativeState = StateHolder.getState(endpoint); expiredNegativeDurationStateHolder.expireState(endpoint); StateHolder.updateState(expiredNegativeDurationStateHolder); - assertEquals(originalExpireNagativeState, StateHolder.getState(endpoint)); + assertEquals(originalExpireNegativeState, StateHolder.getState(endpoint)); } private void updateNextRefreshTimeNoRefreshTest(TestInfo testInfo) { @@ -139,7 +137,8 @@ private void updateNextRefreshBackoffCalcTest(TestInfo testInfo) { try (MockedStatic backoffTimeCalculatorMock = Mockito .mockStatic(BackoffTimeCalculator.class)) { Long ns = Long.valueOf("300000000000"); - backoffTimeCalculatorMock.when(() -> BackoffTimeCalculator.calculateBackoff(Mockito.anyInt())).thenReturn(ns); + backoffTimeCalculatorMock.when(() -> BackoffTimeCalculator.calculateBackoff(Mockito.anyInt())) + .thenReturn(ns); stateHolder.updateNextRefreshTime(null, (long) -120); State newState = StateHolder.getState(endpoint); @@ -169,8 +168,8 @@ private void updateNextRefreshBackoffCalcTest(TestInfo testInfo) { private void loadStateTest(TestInfo testInfo) { String endpoint = testInfo.getDisplayName() + "updateRefreshTimeBackoffCalc" + ".azconfig.io"; StateHolder testStateHolder = new StateHolder(); - testStateHolder.setLoadState(endpoint, true); - testStateHolder.setLoadStateFeatureFlag(endpoint, true); + testStateHolder.setLoadState(endpoint, true, false); + testStateHolder.setLoadStateFeatureFlag(endpoint, true, false); StateHolder.updateState(testStateHolder); assertEquals(testStateHolder.getLoadState().get(endpoint), StateHolder.getLoadState(endpoint)); assertTrue(StateHolder.getLoadStateFeatureFlag(endpoint)); diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/TestConstants.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/TestConstants.java similarity index 98% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/TestConstants.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/TestConstants.java index f23ed0f9131a2..bc331639c468a 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/TestConstants.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/TestConstants.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config; +package com.azure.spring.cloud.config.implementation; /** * Test constants which can be shared across different test classes diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/TestUtils.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/TestUtils.java similarity index 80% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/TestUtils.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/TestUtils.java index 001d08be59c7d..4735feacaaf88 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/TestUtils.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/TestUtils.java @@ -11,9 +11,9 @@ import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; import com.azure.data.appconfiguration.models.FeatureFlagFilter; import com.azure.data.appconfiguration.models.SecretReferenceConfigurationSetting; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreSelects; -import com.azure.spring.cloud.config.properties.ConfigStore; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationKeyValueSelector; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; @@ -33,7 +33,8 @@ public static String propPair(String propName, String propValue) { return String.format("%s=%s", propName, propValue); } - static ConfigurationSetting createItem(String keyFilter, String key, String value, String label, String contentType) { + static ConfigurationSetting createItem(String keyFilter, String key, String value, String label, + String contentType) { ConfigurationSetting item = new ConfigurationSetting(); item.setKey(keyFilter + key); item.setValue(value); @@ -65,8 +66,8 @@ static FeatureFlagConfigurationSetting createItemFeatureFlag(String prefix, Stri Map result = MAPPER.convertValue(nodeParams, new TypeReference>() { }); - Set parameterKeys = result.keySet(); - for (String paramKey : parameterKeys) { + Set parameters = result.keySet(); + for (String paramKey : parameters) { filter.addParameter(paramKey, result.get(paramKey)); } } @@ -79,7 +80,8 @@ static FeatureFlagConfigurationSetting createItemFeatureFlag(String prefix, Stri return item; } - static SecretReferenceConfigurationSetting createSecretReference(String keyFilter, String key, String value, String label, String contentType) { + static SecretReferenceConfigurationSetting createSecretReference(String keyFilter, String key, String value, + String label, String contentType) { SecretReferenceConfigurationSetting item = new SecretReferenceConfigurationSetting(key, value); item.setKey(keyFilter + key); item.setLabel(label); @@ -88,18 +90,21 @@ static SecretReferenceConfigurationSetting createSecretReference(String keyFilte return item; } - static void addStore(AppConfigurationProperties properties, String storeEndpoint, String connectionString, String keyFilter) { + static void addStore(AppConfigurationProperties properties, String storeEndpoint, String connectionString, + String keyFilter) { addStore(properties, storeEndpoint, connectionString, keyFilter, "\0"); } - static void addStore(AppConfigurationProperties properties, String storeEndpoint, String connectionString, String keyFilter, + static void addStore(AppConfigurationProperties properties, String storeEndpoint, String connectionString, + String keyFilter, String label) { List stores = properties.getStores(); ConfigStore store = new ConfigStore(); store.setConnectionString(connectionString); store.setEndpoint(storeEndpoint); - AppConfigurationStoreSelects selectedKeys = new AppConfigurationStoreSelects().setKeyFilter(keyFilter).setLabelFilter(label); - List selects = new ArrayList<>(); + AppConfigurationKeyValueSelector selectedKeys = new AppConfigurationKeyValueSelector().setKeyFilter(keyFilter) + .setLabelFilter(label); + List selects = new ArrayList<>(); selects.add(selectedKeys); store.setSelects(selects); stores.add(store); diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfigurationBootstrapConfigurationTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfigurationTest.java similarity index 68% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfigurationBootstrapConfigurationTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfigurationTest.java index dcd31a319ddc1..b75e30f9f5056 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfigurationBootstrapConfigurationTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfigurationTest.java @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config; +package com.azure.spring.cloud.config.implementation.config; -import static com.azure.spring.cloud.config.TestConstants.CONN_STRING_PROP; -import static com.azure.spring.cloud.config.TestConstants.FAIL_FAST_PROP; -import static com.azure.spring.cloud.config.TestConstants.STORE_ENDPOINT_PROP; -import static com.azure.spring.cloud.config.TestConstants.TEST_CONN_STRING; -import static com.azure.spring.cloud.config.TestConstants.TEST_STORE_NAME; +import static com.azure.spring.cloud.config.implementation.TestConstants.CONN_STRING_PROP; +import static com.azure.spring.cloud.config.implementation.TestConstants.FAIL_FAST_PROP; +import static com.azure.spring.cloud.config.implementation.TestConstants.STORE_ENDPOINT_PROP; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_CONN_STRING; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_STORE_NAME; import static com.azure.spring.cloud.config.implementation.TestUtils.propPair; import static org.assertj.core.api.Assertions.assertThat; @@ -14,13 +14,16 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import com.azure.spring.cloud.autoconfigure.context.AzureGlobalPropertiesAutoConfiguration; import com.azure.spring.cloud.config.implementation.AppConfigurationPropertySourceLocator; import com.azure.spring.cloud.config.implementation.AppConfigurationReplicaClientFactory; public class AppConfigurationBootstrapConfigurationTest { private static final ApplicationContextRunner CONTEXT_RUNNER = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(AppConfigurationBootstrapConfiguration.class)); + .withConfiguration(AutoConfigurations.of(AppConfigurationBootstrapConfiguration.class, + AzureGlobalPropertiesAutoConfiguration.class)) + .withPropertyValues(propPair("spring.cloud.azure.appconfiguration.enabled", "true")); @Test public void iniConnectionStringSystemAssigned() { @@ -47,8 +50,7 @@ public void propertySourceLocatorBeanCreated() { @Test public void clientsBeanCreated() { CONTEXT_RUNNER - .withPropertyValues(propPair(CONN_STRING_PROP, TEST_CONN_STRING)).run(context -> { - assertThat(context).hasSingleBean(AppConfigurationReplicaClientFactory.class); - }); + .withPropertyValues(propPair(CONN_STRING_PROP, TEST_CONN_STRING)) + .run(context -> assertThat(context).hasSingleBean(AppConfigurationReplicaClientFactory.class)); } } diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/pipline/policies/BaseAppConfigurationPolicyTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/pipline/policies/BaseAppConfigurationPolicyTest.java similarity index 91% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/pipline/policies/BaseAppConfigurationPolicyTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/pipline/policies/BaseAppConfigurationPolicyTest.java index 5c68863d204b3..e07be252a1ae9 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/pipline/policies/BaseAppConfigurationPolicyTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/pipline/policies/BaseAppConfigurationPolicyTest.java @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.pipline.policies; +package com.azure.spring.cloud.config.implementation.pipline.policies; -import static com.azure.spring.cloud.config.AppConfigurationConstants.CORRELATION_CONTEXT; -import static com.azure.spring.cloud.config.AppConfigurationConstants.DEV_ENV_TRACING; -import static com.azure.spring.cloud.config.AppConfigurationConstants.KEY_VAULT_CONFIGURED_TRACING; -import static com.azure.spring.cloud.config.AppConfigurationConstants.USER_AGENT_TYPE; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.CORRELATION_CONTEXT; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.DEV_ENV_TRACING; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.KEY_VAULT_CONFIGURED_TRACING; +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.USER_AGENT_TYPE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfigurationPropertiesTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationPropertiesTest.java similarity index 65% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfigurationPropertiesTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationPropertiesTest.java index 5602095082762..e92045b2dd1f5 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfigurationPropertiesTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationPropertiesTest.java @@ -1,8 +1,26 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config; +package com.azure.spring.cloud.config.implementation.properties; + +import static com.azure.spring.cloud.config.implementation.AppConfigurationReplicaClientsBuilder.ENDPOINT_ERR_MSG; +import static com.azure.spring.cloud.config.implementation.TestConstants.CONN_STRING_PROP; +import static com.azure.spring.cloud.config.implementation.TestConstants.CONN_STRING_PROP_NEW; +import static com.azure.spring.cloud.config.implementation.TestConstants.FAIL_FAST_PROP; +import static com.azure.spring.cloud.config.implementation.TestConstants.KEY_PROP; +import static com.azure.spring.cloud.config.implementation.TestConstants.LABEL_PROP; +import static com.azure.spring.cloud.config.implementation.TestConstants.REFRESH_INTERVAL_PROP; +import static com.azure.spring.cloud.config.implementation.TestConstants.STORE_ENDPOINT_PROP; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_CONN_STRING; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_ENDPOINT; +import static com.azure.spring.cloud.config.implementation.TestConstants.TEST_ENDPOINT_GEO; +import static com.azure.spring.cloud.config.implementation.TestUtils.propPair; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.List; -import com.azure.spring.cloud.config.properties.AppConfigurationProperties; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -11,10 +29,8 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import static com.azure.spring.cloud.config.TestConstants.*; -import static com.azure.spring.cloud.config.implementation.AppConfigurationReplicaClientsBuilder.ENDPOINT_ERR_MSG; -import static com.azure.spring.cloud.config.implementation.TestUtils.propPair; -import static org.assertj.core.api.Assertions.assertThat; +import com.azure.spring.cloud.autoconfigure.context.AzureGlobalPropertiesAutoConfiguration; +import com.azure.spring.cloud.config.implementation.config.AppConfigurationBootstrapConfiguration; public class AppConfigurationPropertiesTest { @@ -30,7 +46,9 @@ public class AppConfigurationPropertiesTest { @InjectMocks private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(AppConfigurationBootstrapConfiguration.class)); + .withConfiguration(AutoConfigurations.of(AppConfigurationBootstrapConfiguration.class, + AzureGlobalPropertiesAutoConfiguration.class)) + .withPropertyValues("spring.cloud.azure.appconfiguration.endpoint=https://test-appconfig.azconfig.io"); @BeforeEach public void setup() { @@ -116,4 +134,27 @@ public void minValidWatchTime() { .withPropertyValues(propPair(REFRESH_INTERVAL_PROP, "1s")) .run(context -> assertThat(context).hasSingleBean(AppConfigurationProperties.class)); } + + @Test + public void multipleEndpointsTest() { + AppConfigurationProperties properties = new AppConfigurationProperties(); + ConfigStore store = new ConfigStore(); + List endpoints = new ArrayList<>(); + endpoints.add(TEST_ENDPOINT); + endpoints.add(TEST_ENDPOINT_GEO); + + store.setEndpoints(endpoints); + List stores = new ArrayList<>(); + stores.add(store); + + properties.setStores(stores); + properties.validateAndInit(); + + endpoints.clear(); + endpoints.add(TEST_ENDPOINT); + endpoints.add(TEST_ENDPOINT); + + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> properties.validateAndInit()); + assertEquals("Duplicate store name exists.", e.getMessage()); + } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationStoreMonitoringTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationStoreMonitoringTest.java new file mode 100644 index 0000000000000..38f7be707f2bb --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationStoreMonitoringTest.java @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation.properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +public class AppConfigurationStoreMonitoringTest { + + @Test + public void validateAndInitTest() { + // Disabled anything is fine + AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); + monitoring.validateAndInit(); + + // Enabled throw error if no triggers + monitoring.setEnabled(true); + assertThrows(IllegalArgumentException.class, () -> monitoring.validateAndInit()); + + List triggers = new ArrayList<>(); + monitoring.setTriggers(triggers); + + assertThrows(IllegalArgumentException.class, () -> monitoring.validateAndInit()); + + AppConfigurationStoreTrigger trigger = new AppConfigurationStoreTrigger(); + trigger.setKey("sentinal"); + + triggers.add(trigger); + monitoring.setTriggers(triggers); + monitoring.validateAndInit(); + + monitoring.setRefreshInterval(Duration.ofSeconds(0)); + + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> monitoring.validateAndInit()); + assertEquals("Minimum refresh interval time is 1 Second.", e.getMessage()); + + monitoring.setRefreshInterval(Duration.ofSeconds(1)); + monitoring.setFeatureFlagRefreshInterval(Duration.ofSeconds(0)); + + e = assertThrows(IllegalArgumentException.class, () -> monitoring.validateAndInit()); + assertEquals("Minimum Feature Flag refresh interval time is 1 Second.", e.getMessage()); + + monitoring.setFeatureFlagRefreshInterval(Duration.ofSeconds(1)); + + monitoring.validateAndInit(); + } + +} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/properties/AppConfigurationStoreSelectsTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationStoreSelectsTest.java similarity index 74% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/properties/AppConfigurationStoreSelectsTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationStoreSelectsTest.java index 0388183828742..8994bd1c14887 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/properties/AppConfigurationStoreSelectsTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationStoreSelectsTest.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.properties; +package com.azure.spring.cloud.config.implementation.properties; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -14,7 +14,7 @@ public class AppConfigurationStoreSelectsTest { @Test public void labelOverProfiles() { - AppConfigurationStoreSelects selects = new AppConfigurationStoreSelects().setLabelFilter("v1"); + AppConfigurationKeyValueSelector selects = new AppConfigurationKeyValueSelector().setLabelFilter("v1"); List profiles = new ArrayList<>(); profiles.add("dev"); @@ -27,7 +27,7 @@ public void labelOverProfiles() { @Test public void useProfiles() { - AppConfigurationStoreSelects selects = new AppConfigurationStoreSelects(); + AppConfigurationKeyValueSelector selects = new AppConfigurationKeyValueSelector(); List profiles = new ArrayList<>(); profiles.add("dev"); @@ -40,7 +40,7 @@ public void useProfiles() { @Test public void defaultCase() { - AppConfigurationStoreSelects selects = new AppConfigurationStoreSelects(); + AppConfigurationKeyValueSelector selects = new AppConfigurationKeyValueSelector(); String[] results = selects.getLabelFilter(new ArrayList<>()); assertEquals(1, results.length); @@ -50,7 +50,7 @@ public void defaultCase() { @Test public void emptyCases() { - AppConfigurationStoreSelects selects = new AppConfigurationStoreSelects().setLabelFilter(" "); + AppConfigurationKeyValueSelector selects = new AppConfigurationKeyValueSelector().setLabelFilter(" "); String[] results = selects.getLabelFilter(new ArrayList<>()); assertEquals(1, results.length); @@ -67,7 +67,7 @@ public void emptyCases() { @Test public void multipleLabels() { - AppConfigurationStoreSelects selects = new AppConfigurationStoreSelects().setLabelFilter("dev,test"); + AppConfigurationKeyValueSelector selects = new AppConfigurationKeyValueSelector().setLabelFilter("dev,test"); String[] results = selects.getLabelFilter(new ArrayList<>()); assertEquals(2, results.length); @@ -96,7 +96,7 @@ public void multipleLabels() { @Test public void workaroundForEmptyLabelConfig() { - AppConfigurationStoreSelects selects = new AppConfigurationStoreSelects().setLabelFilter("v1,"); + AppConfigurationKeyValueSelector selects = new AppConfigurationKeyValueSelector().setLabelFilter("v1,"); String[] results = selects.getLabelFilter(new ArrayList<>()); assertEquals(2, results.length); @@ -107,20 +107,20 @@ public void workaroundForEmptyLabelConfig() { @Test public void invalidCharacters() { - AppConfigurationStoreSelects selects = new AppConfigurationStoreSelects().setLabelFilter("v1*"); + AppConfigurationKeyValueSelector selects = new AppConfigurationKeyValueSelector().setLabelFilter("v1*"); String[] results = selects.getLabelFilter(new ArrayList<>()); assertEquals(1, results.length); assertEquals("v1*", results[0]); - assertThrows(IllegalArgumentException.class, () -> selects.validateAndInit()); + assertThrows(IllegalArgumentException.class, selects::validateAndInit); - AppConfigurationStoreSelects selects2 = new AppConfigurationStoreSelects().setLabelFilter("v1") + AppConfigurationKeyValueSelector selects2 = new AppConfigurationKeyValueSelector().setLabelFilter("v1") .setKeyFilter("/application/*"); results = selects2.getLabelFilter(new ArrayList<>()); assertEquals(1, results.length); assertEquals("v1", results[0]); - assertThrows(IllegalArgumentException.class, () -> selects2.validateAndInit()); + assertThrows(IllegalArgumentException.class, selects2::validateAndInit); } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagKeyValueSelectorTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagKeyValueSelectorTest.java new file mode 100644 index 0000000000000..ed2908ce62721 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagKeyValueSelectorTest.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation.properties; + +import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.EMPTY_LABEL; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +public class FeatureFlagKeyValueSelectorTest { + + @Test + public void validateAndInitTest() { + FeatureFlagKeyValueSelector selector = new FeatureFlagKeyValueSelector(); + selector.validateAndInit(); + + selector.setLabelFilter("de*"); + + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> selector.validateAndInit()); + assertEquals("LabelFilter must not contain asterisk(*)", e.getMessage()); + + selector.setLabelFilter("dev"); + selector.validateAndInit(); + } + + @Test + public void getLabelFilterTest() { + // Default is Empty Label + FeatureFlagKeyValueSelector selector = new FeatureFlagKeyValueSelector(); + selector.validateAndInit(); + + List profiles = new ArrayList<>(); + + String[] labels = selector.getLabelFilter(profiles); + + assertEquals(1, labels.length); + assertTrue(EMPTY_LABEL.equalsIgnoreCase(labels[0])); + + // Uses the profile + profiles.add("dev"); + labels = selector.getLabelFilter(profiles); + + assertEquals(1, labels.length); + assertEquals("dev", labels[0]); + + // Label should override profile + selector.setLabelFilter("test"); + labels = selector.getLabelFilter(profiles); + + assertEquals(1, labels.length); + assertEquals("test", labels[0]); + + // Multiple Labels, List is reversed as high number will have priority + selector.setLabelFilter("test1, test2"); + labels = selector.getLabelFilter(profiles); + + assertEquals(2, labels.length); + assertEquals("test2", labels[0]); + assertEquals("test1", labels[1]); + + // Multiple Labels, Ending with a comma results in a null label + selector.setLabelFilter("test1,"); + labels = selector.getLabelFilter(profiles); + + assertEquals(2, labels.length); + assertEquals(EMPTY_LABEL, labels[0]); + assertEquals("test1", labels[1]); + } + +} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagStoreTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagStoreTest.java new file mode 100644 index 0000000000000..29c5e15aa3d1e --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/properties/FeatureFlagStoreTest.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation.properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +public class FeatureFlagStoreTest { + + @Test + public void validateAndInitTest() { + // Starts out empty + FeatureFlagStore featureStore = new FeatureFlagStore(); + assertEquals(0, featureStore.getSelects().size()); + + // If disabled does't setup the select all f t + featureStore = new FeatureFlagStore(); + featureStore.validateAndInit(); + assertEquals(0, featureStore.getSelects().size()); + + // Enabled, with no selects, so selector is created t t + featureStore = new FeatureFlagStore(); + featureStore.setEnabled(true); + featureStore.validateAndInit(); + assertEquals(1, featureStore.getSelects().size()); + assertEquals("", featureStore.getSelects().get(0).getKeyFilter()); + + featureStore = new FeatureFlagStore(); + featureStore.setEnabled(true); + + List selectors = new ArrayList<>(); + FeatureFlagKeyValueSelector selector = new FeatureFlagKeyValueSelector(); + selector.setKeyFilter(".appconfig/Alpha"); + selectors.add(selector); + featureStore.setSelects(selectors); + + featureStore.validateAndInit(); + assertEquals(1, featureStore.getSelects().size()); + assertEquals(".appconfig/Alpha", featureStore.getSelects().get(0).getKeyFilter()); + } +} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/stores/ConfigStoreTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/stores/ConfigStoreTest.java similarity index 59% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/stores/ConfigStoreTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/stores/ConfigStoreTest.java index 4edd835c6a7cf..9914d2b7cb886 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/stores/ConfigStoreTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/stores/ConfigStoreTest.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.stores; +package com.azure.spring.cloud.config.implementation.stores; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -11,32 +11,32 @@ import org.junit.jupiter.api.Test; -import com.azure.spring.cloud.config.properties.AppConfigurationStoreSelects; -import com.azure.spring.cloud.config.properties.ConfigStore; +import com.azure.spring.cloud.config.implementation.properties.AppConfigurationKeyValueSelector; +import com.azure.spring.cloud.config.implementation.properties.ConfigStore; public class ConfigStoreTest { @Test public void invalidLabel() { ConfigStore configStore = new ConfigStore(); - AppConfigurationStoreSelects selectedKeys = new AppConfigurationStoreSelects().setKeyFilter("/application/") + AppConfigurationKeyValueSelector selectedKeys = new AppConfigurationKeyValueSelector().setKeyFilter("/application/") .setLabelFilter("*"); - List selects = new ArrayList<>(); + List selects = new ArrayList<>(); selects.add(selectedKeys); configStore.setSelects(selects); - assertThrows(IllegalArgumentException.class, () -> configStore.validateAndInit()); + assertThrows(IllegalArgumentException.class, configStore::validateAndInit); } @Test public void invalidKey() { ConfigStore configStore = new ConfigStore(); - AppConfigurationStoreSelects selectedKeys = new AppConfigurationStoreSelects().setKeyFilter("/application/*"); - List selects = new ArrayList<>(); + AppConfigurationKeyValueSelector selectedKeys = new AppConfigurationKeyValueSelector().setKeyFilter("/application/*"); + List selects = new ArrayList<>(); selects.add(selectedKeys); configStore.setSelects(selects); - - assertThrows(IllegalArgumentException.class, () -> configStore.validateAndInit()); + + assertThrows(IllegalArgumentException.class, configStore::validateAndInit); } @Test @@ -45,7 +45,7 @@ public void invalidEndpoint() { configStore.validateAndInit(); configStore.setConnectionString("Endpoint=a^a;Id=fake-conn-id;Secret=ZmFrZS1jb25uLXNlY3JldA=="); - assertThrows(IllegalStateException.class, () -> configStore.validateAndInit()); + assertThrows(IllegalStateException.class, configStore::validateAndInit); } @Test @@ -55,21 +55,21 @@ public void getLabelsTest() { assertEquals("\0", configStore.getSelects().get(0).getLabelFilter(new ArrayList<>())[0]); - AppConfigurationStoreSelects selectedKeys = new AppConfigurationStoreSelects().setKeyFilter("/application/") + AppConfigurationKeyValueSelector selectedKeys = new AppConfigurationKeyValueSelector().setKeyFilter("/application/") .setLabelFilter("dev"); - List selects = new ArrayList<>(); + List selects = new ArrayList<>(); selects.add(selectedKeys); configStore.setSelects(selects); assertEquals("dev", configStore.getSelects().get(0).getLabelFilter(new ArrayList<>())[0]); - selectedKeys = new AppConfigurationStoreSelects().setKeyFilter("/application/").setLabelFilter("dev,test"); + selectedKeys = new AppConfigurationKeyValueSelector().setKeyFilter("/application/").setLabelFilter("dev,test"); selects = new ArrayList<>(); selects.add(selectedKeys); configStore.setSelects(selects); assertEquals("test", configStore.getSelects().get(0).getLabelFilter(new ArrayList<>())[0]); assertEquals("dev", configStore.getSelects().get(0).getLabelFilter(new ArrayList<>())[1]); - selectedKeys = new AppConfigurationStoreSelects().setKeyFilter("/application/").setLabelFilter(","); + selectedKeys = new AppConfigurationKeyValueSelector().setKeyFilter("/application/").setLabelFilter(","); selects = new ArrayList<>(); selects.add(selectedKeys); configStore.setSelects(selects); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/stores/KeyVaultClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/stores/KeyVaultClientTest.java new file mode 100644 index 0000000000000..3183c8414d407 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/stores/KeyVaultClientTest.java @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.config.implementation.stores; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import com.azure.core.credential.TokenCredential; +import com.azure.security.keyvault.secrets.SecretAsyncClient; +import com.azure.security.keyvault.secrets.SecretClientBuilder; +import com.azure.security.keyvault.secrets.models.KeyVaultSecret; +import com.azure.spring.cloud.config.KeyVaultSecretProvider; +import com.azure.spring.cloud.service.implementation.keyvault.secrets.SecretClientBuilderFactory; + +import reactor.core.publisher.Mono; + +public class KeyVaultClientTest { + + private AppConfigurationSecretClientManager clientStore; + + @Mock + private SecretClientBuilder builderMock; + + @Mock + private SecretAsyncClient clientMock; + + @Mock + private TokenCredential credentialMock; + + @Mock + private Mono monoSecret; + + @Mock + private SecretClientBuilderFactory secretClientBuilderFactoryMock; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @AfterEach + public void cleanup() throws Exception { + MockitoAnnotations.openMocks(this).close(); + } + + @Test + public void configProviderAuth() throws URISyntaxException { + String keyVaultUri = "https://keyvault.vault.azure.net"; + + clientStore = new AppConfigurationSecretClientManager(keyVaultUri, null, null, secretClientBuilderFactoryMock, + false); + + AppConfigurationSecretClientManager test = Mockito.spy(clientStore); + when(secretClientBuilderFactoryMock.build()).thenReturn(builderMock); + + when(builderMock.vaultUrl(Mockito.any())).thenReturn(builderMock); + when(builderMock.buildAsyncClient()).thenReturn(clientMock); + + test.build(); + + when(clientMock.getSecret(Mockito.any(), Mockito.any())) + .thenReturn(monoSecret); + when(monoSecret.block(Mockito.any())).thenReturn(new KeyVaultSecret("", "")); + + assertNotNull(test.getSecret(new URI(keyVaultUri), 10)); + assertEquals(test.getSecret(new URI(keyVaultUri), 10).getName(), ""); + } + + @Test + public void systemAssignedCredentials() throws URISyntaxException { + String keyVaultUri = "https://keyvault.vault.azure.net/secrets/mySecret"; + + clientStore = new AppConfigurationSecretClientManager(keyVaultUri, null, null, secretClientBuilderFactoryMock, + false); + + AppConfigurationSecretClientManager test = Mockito.spy(clientStore); + when(secretClientBuilderFactoryMock.build()).thenReturn(builderMock); + + when(builderMock.vaultUrl(Mockito.any())).thenReturn(builderMock); + when(builderMock.buildAsyncClient()).thenReturn(clientMock); + + test.build(); + + when(clientMock.getSecret(Mockito.any(), Mockito.any())) + .thenReturn(monoSecret); + when(monoSecret.block(Mockito.any())).thenReturn(new KeyVaultSecret("", "")); + + assertNotNull(test.getSecret(new URI(keyVaultUri), 10)); + assertEquals(test.getSecret(new URI(keyVaultUri), 10).getName(), ""); + } + + @Test + public void secretResolverTest() throws URISyntaxException { + String keyVaultUri = "https://keyvault.vault.azure.net/secrets/mySecret"; + + clientStore = new AppConfigurationSecretClientManager(keyVaultUri, null, new TestSecretResolver(), + secretClientBuilderFactoryMock, false); + + AppConfigurationSecretClientManager test = Mockito.spy(clientStore); + when(secretClientBuilderFactoryMock.build()).thenReturn(builderMock); + + when(builderMock.vaultUrl(Mockito.any())).thenReturn(builderMock); + + assertEquals("Test-Value", test.getSecret(new URI(keyVaultUri + "/testSecret"), 10).getValue()); + assertEquals("Default-Secret", test.getSecret(new URI(keyVaultUri + "/testSecret2"), 10).getValue()); + } + + class TestSecretResolver implements KeyVaultSecretProvider { + + @Override + public String getSecret(String uri) { + if (uri.endsWith("/testSecret")) { + return "Test-Value"; + } + return "Default-Secret"; + } + + } +} diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/resources/jsonContentTypeData.json b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/resources/jsonContentTypeData.json similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/resources/jsonContentTypeData.json rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/resources/jsonContentTypeData.json diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/CHANGELOG.md b/sdk/spring/spring-cloud-azure-feature-management-web/CHANGELOG.md similarity index 88% rename from sdk/appconfiguration/azure-spring-cloud-feature-management-web/CHANGELOG.md rename to sdk/spring/spring-cloud-azure-feature-management-web/CHANGELOG.md index 446c0485ede77..2787b5b9ecd94 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-feature-management-web/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 2.11.0-beta.1 (Unreleased) +## 4.0.0-beta.3 (Unreleased) ### Features Added @@ -10,12 +10,12 @@ ### Other Changes -## 2.10.0 (2023-01-18) -Upgrade Spring Boot dependencies version to 2.7.7 and Spring Cloud dependencies version to 2021.0.5 +## 4.0.0-beta.2 (2022-10-06) -## 2.9.0 (2022-11-24) -- This release is compatible with Spring Boot 2.5.0-2.5.14, 2.6.0-2.6.13, 2.7.0-2.7.5. (Note: 2.5.x (x>14), 2.6.y (y>13) and 2.7.z (z>5) should be supported, but they aren't tested with this release.) -- This release is compatible with Spring Cloud 2020.0.3-2020.0.6, 2021.0.0-2021.0.5. (Note: 2020.0.x (x>6) and 2021.0.y (y>5) should be supported, but they aren't tested with this release.) +- Dynamic Feature release with: + - Geo-replication + - Spring Boot 2.5.0-2.5.14, 2.6.0-2.6.11, 2.7.0-2.7.3. (Note: 2.5.x (x>14), 2.6.y (y>11) and 2.7.z (z>3) should be supported, but they aren't tested with this release.) + - Spring Cloud 2020.0.3-2020.0.6, 2021.0.0-2021.0.3. (Note: 2020.0.x (x>6) and 2021.0.y (y>3) should be supported, but they aren't tested with this release.) ## 2.8.0 (2022-09-22) - This release is compatible with Spring Boot 2.5.0-2.5.14, 2.6.0-2.6.11, 2.7.0-2.7.3. (Note: 2.5.x (x>14), 2.6.y (y>11) and 2.7.z (z>3) should be supported, but they aren't tested with this release.) @@ -31,6 +31,11 @@ Upgrade Spring Boot dependencies version to 2.7.7 and Spring Cloud dependencies ### Dependency Upgrades - Upgrade azure-sdk's version to latest released version. +## 4.0.0-beta.1 (2022-06-21) + +- Adds Support for Dynamic Features. +- Updated to use both the old and new Feature Management schema. + ## 2.6.0 (2022-05-24) - This release is compatible with Spring Boot 2.5.0-2.5.13, 2.6.0-2.6.7. (Note: 2.5.x (x>13) and 2.6.y (y>7) should be supported, but they aren't tested with this release.) - This release is compatible with Spring Cloud 2020.0.3-2020.0.5, 2021.0.0-2021.0.2. (Note: 2020.0.x (x>5) and 2021.0.y (y>2) should be supported, but they aren't tested with this release.) diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/README.md b/sdk/spring/spring-cloud-azure-feature-management-web/README.md similarity index 51% rename from sdk/appconfiguration/azure-spring-cloud-feature-management-web/README.md rename to sdk/spring/spring-cloud-azure-feature-management-web/README.md index 2a393af67b905..e5d2241ad891d 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/README.md +++ b/sdk/spring/spring-cloud-azure-feature-management-web/README.md @@ -1,3 +1,3 @@ # Spring Cloud for Azure feature management web client library for Java -See: [Spring Cloud Azure Feature Management](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/appconfiguration/azure-spring-cloud-feature-management) \ No newline at end of file +See: [Spring Cloud Azure Feature Management](https://github.com/Azure/azure-sdk-for-java/blob/feature/azconfig-spring/DynamicFeature/sdk/appconfiguration/spring-cloud-azure-feature-management) diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/pom.xml b/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml similarity index 65% rename from sdk/appconfiguration/azure-spring-cloud-feature-management-web/pom.xml rename to sdk/spring/spring-cloud-azure-feature-management-web/pom.xml index 857becccb026f..51a5030b3bca6 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/pom.xml +++ b/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml @@ -1,6 +1,4 @@ - + com.azure azure-client-sdk-parent @@ -10,31 +8,40 @@ 4.0.0 com.azure.spring - azure-spring-cloud-feature-management-web - 2.11.0-beta.1 - Azure Spring Cloud Feature Management Web + spring-cloud-azure-feature-management-web + 4.0.0-beta.3 + Spring Cloud Azure Feature Management Web Adds Feature Management into Spring Web - - - true - + + + scm:git:git@github.com:Azure/azure-sdk-for-java.git + scm:git:ssh://git@github.com:Azure/azure-sdk-for-java.git + https://github.com/Azure/azure-sdk-for-java + + + + + microsoft + Microsoft Corporation + + org.springframework.boot spring-boot-starter-test - 2.7.8 + 2.7.4 test org.springframework spring-web - 5.3.25 + 5.3.23 org.springframework spring-webmvc - 5.3.25 + 5.3.23 javax.servlet @@ -44,8 +51,8 @@ com.azure.spring - azure-spring-cloud-feature-management - 2.11.0-beta.1 + spring-cloud-azure-feature-management + 4.0.0-beta.3 + com.azure.spring:spring-cloud-azure-feature-management:[4.0.0-beta.3] javax.servlet:javax.servlet-api:[4.0.1] - org.springframework:spring-web:[5.3.25] - org.springframework:spring-webmvc:[5.3.25] + org.springframework:spring-web:[5.3.23] + org.springframework:spring-webmvc:[5.3.23] @@ -84,4 +91,4 @@ - + \ No newline at end of file diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshot.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshot.java new file mode 100644 index 0000000000000..e075d89c9223b --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshot.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager; + +import java.util.HashMap; +import java.util.Map; + +import reactor.core.publisher.Mono; + +/** + * Holds information on Feature Management properties and can check if a given feature is enabled. Returns the same + * value in the same request. + */ +public class DynamicFeatureManagerSnapshot { + + private final DynamicFeatureManager dynamicFeatureManager; + + private final Map requestMap; + + /** + * Used to evaluate whether a feature is enabled or disabled. When setup with the @RequestScope it will + * return the same value for all checks of the given feature flag. + * + * @param dynamicFeatureManager DynamicFeatureManager + */ + public DynamicFeatureManagerSnapshot(DynamicFeatureManager dynamicFeatureManager) { + this.dynamicFeatureManager = dynamicFeatureManager; + this.requestMap = new HashMap<>(); + } + + /** + * Returns a feature variant of the type given. + *

+ * If getVariantAsync has all ready been called on this variant, it will return the same variant as it did before. + * + * @param Type of the feature that will be returned. + * @param variantName name of the variant being checked. + * @param type Type of the feature being checked. + * @return variant of the provided type, can return Mono with a null value if the feature requested doesn't match + * the stored type. + * @throws FeatureManagementException A generic exception for when something goes wrong with the variant generation + */ + public Mono getVariantAsync(String variantName, Class type) + throws FeatureManagementException { + if (!requestMap.containsKey(variantName)) { + return dynamicFeatureManager.getVariantAsync(variantName, type).doOnSuccess(newVariant -> { + requestMap.put(variantName, newVariant); + }); + } + + T variant = null; + Object o = requestMap.get(variantName); + if (o.getClass().equals(type)) { + variant = type.cast(o); + } + + return Mono.justOrEmpty(variant); + } +} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureGate.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureGate.java similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureGate.java rename to sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureGate.java diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureHandler.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureHandler.java similarity index 95% rename from sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureHandler.java rename to sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureHandler.java index 27816c7b0e528..5ea69e808846f 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureHandler.java +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureHandler.java @@ -26,11 +26,11 @@ public class FeatureHandler extends HandlerInterceptorAdapter { private static final Logger LOGGER = LoggerFactory.getLogger(FeatureHandler.class); - private FeatureManager featureManager; + private final FeatureManager featureManager; - private FeatureManagerSnapshot featureManagerSnapshot; + private final FeatureManagerSnapshot featureManagerSnapshot; - private IDisabledFeaturesHandler disabledFeaturesHandler; + private final IDisabledFeaturesHandler disabledFeaturesHandler; /** * Interceptor for Requests to check if they should be run. diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagerSnapshot.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagerSnapshot.java similarity index 90% rename from sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagerSnapshot.java rename to sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagerSnapshot.java index 2d15899de8151..ba19154db11b3 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagerSnapshot.java +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagerSnapshot.java @@ -4,20 +4,17 @@ import java.util.HashMap; -import org.springframework.context.annotation.Configuration; - import reactor.core.publisher.Mono; /** * Holds information on Feature Management properties and can check if a given feature is enabled. Returns the same * value in the same request. */ -@Configuration public class FeatureManagerSnapshot { - private FeatureManager featureManager; + private final FeatureManager featureManager; - private HashMap requestMap; + private final HashMap requestMap; /** * Used to evaluate whether a feature is enabled or disabled. When setup with the @RequestScope it will diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/IDisabledFeaturesHandler.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/IDisabledFeaturesHandler.java similarity index 95% rename from sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/IDisabledFeaturesHandler.java rename to sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/IDisabledFeaturesHandler.java index f3a815f1769c4..492ac9fd866e5 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/IDisabledFeaturesHandler.java +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/IDisabledFeaturesHandler.java @@ -16,7 +16,7 @@ public interface IDisabledFeaturesHandler { /** * Called when an endpoint intercepter returns and no redirect is set. * - * @param request current HTTP + * @param request current HTTP * @param response current HTTP * @return response to current HTTP request */ diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureConfig.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureConfig.java similarity index 88% rename from sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureConfig.java rename to sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureConfig.java index 78d315ff6cf52..115ffb49106da 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureConfig.java +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureConfig.java @@ -1,11 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager; +package com.azure.spring.cloud.feature.manager.implementation; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import com.azure.spring.cloud.feature.manager.FeatureHandler; + /** * Adds the feature management handler to intercept all paths. */ diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementWebConfiguration.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementWebConfiguration.java similarity index 64% rename from sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementWebConfiguration.java rename to sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementWebConfiguration.java index 849a89e49d803..0f9169271e1b2 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementWebConfiguration.java +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementWebConfiguration.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager; +package com.azure.spring.cloud.feature.manager.implementation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -9,6 +9,13 @@ import org.springframework.context.annotation.Configuration; import org.springframework.web.context.annotation.RequestScope; +import com.azure.spring.cloud.feature.manager.DynamicFeatureManager; +import com.azure.spring.cloud.feature.manager.DynamicFeatureManagerSnapshot; +import com.azure.spring.cloud.feature.manager.FeatureHandler; +import com.azure.spring.cloud.feature.manager.FeatureManager; +import com.azure.spring.cloud.feature.manager.FeatureManagerSnapshot; +import com.azure.spring.cloud.feature.manager.IDisabledFeaturesHandler; + /** * Configurations setting up FeatureManagerSnapshot, FeatureHandler, FeatureConfig */ @@ -19,6 +26,7 @@ public class FeatureManagementWebConfiguration { /** * Creates FeatureManagerSnapshot + * * @param featureManager App Configuration Feature Manager * @return FeatureManagerSnapshot */ @@ -28,8 +36,21 @@ public FeatureManagerSnapshot featureManagerSnapshot(FeatureManager featureManag return new FeatureManagerSnapshot(featureManager); } + /** + * Creates DynamicFeatureManagerSnapshot + * + * @param dynamicFeatureManager App Configuration Dynamic Feature Manager + * @return DynamicFeatureManagerSnapshot + */ + @Bean + @RequestScope + public DynamicFeatureManagerSnapshot dynamicFeatureManagerSnapshot(DynamicFeatureManager dynamicFeatureManager) { + return new DynamicFeatureManagerSnapshot(dynamicFeatureManager); + } + /** * Creates FeatureHandler + * * @param featureManager App Configuration Feature Manager * @param snapshot App Configuration Feature Manager snapshot version * @param disabledFeaturesHandler optional handler for redirection of disabled endpoints @@ -43,7 +64,8 @@ public FeatureHandler featureHandler(FeatureManager featureManager, FeatureManag /** * Creates FeatureConfig - * @param featureHandler Interceptor for requests to check if then need to be blocked/redirected. + * + * @param featureHandler Interceptor for requests to check if then need to be blocked/redirected. * @return FeatureConfig */ @Bean diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java new file mode 100644 index 0000000000000..98727121d121b --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager.implementation; diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/package-info.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/package-info.java new file mode 100644 index 0000000000000..8163a2f06100f --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package contains classes for accessing and creating Feature Flags, Feature Filters, and Feature Variants with + * integrations with Spring Web. + */ +package com.azure.spring.cloud.feature.manager; diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/resources/META-INF/spring.factories b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000000..ca5c1435b7fd9 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.azure.spring.cloud.feature.manager.implementation.FeatureManagementWebConfiguration \ No newline at end of file diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshotTest.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshotTest.java new file mode 100644 index 0000000000000..32ea92f2faedd --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshotTest.java @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.ExecutionException; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import reactor.core.publisher.Mono; + +public class DynamicFeatureManagerSnapshotTest { + + @Mock + DynamicFeatureManager dynamicFeatureManager; + + @Mock + HttpServletRequest request; + + @InjectMocks + DynamicFeatureManagerSnapshot dynamicFeatureManagerSnapshot; + + @BeforeEach + public void setup(TestInfo testInfo) { + MockitoAnnotations.openMocks(this); + } + + @Test + public void initialLoad() + throws InterruptedException, ExecutionException, FilterNotFoundException, FeatureManagementException { + when(dynamicFeatureManager.getVariantAsync(Mockito.matches("myVariant"), Mockito.any())) + .thenReturn(Mono.just(new Object())); + + Object returnValue = dynamicFeatureManagerSnapshot.getVariantAsync("myVariant", Object.class).block(); + assertNotNull(returnValue); + verify(dynamicFeatureManager, times(1)).getVariantAsync("myVariant", Object.class); + } + + @Test + public void initialAlreadyExists() throws InterruptedException, ExecutionException, FilterNotFoundException, FeatureManagementException { + when(dynamicFeatureManager.getVariantAsync(Mockito.matches("exitingVariant"), Mockito.any())) + .thenReturn(Mono.just(new Object())); + + Object returnValue = dynamicFeatureManagerSnapshot.getVariantAsync("exitingVariant", Object.class).block(); + assertNotNull(returnValue); + verify(dynamicFeatureManager, times(1)).getVariantAsync("exitingVariant", Object.class); + + returnValue = dynamicFeatureManagerSnapshot.getVariantAsync("exitingVariant", Object.class).block(); + assertNotNull(returnValue); + verify(dynamicFeatureManager, times(1)).getVariantAsync("exitingVariant", Object.class); + } + + @Test + public void invalidType() + throws InterruptedException, ExecutionException, FilterNotFoundException, FeatureManagementException { + when(dynamicFeatureManager.getVariantAsync(Mockito.matches("exitingVariant"), Mockito.any())) + .thenReturn(Mono.just(1)); + + Object returnValue = dynamicFeatureManagerSnapshot.getVariantAsync("exitingVariant", Integer.class).block(); + assertEquals(1, returnValue); + returnValue = dynamicFeatureManagerSnapshot.getVariantAsync("exitingVariant", String.class).block(); + assertNull(returnValue); + verify(dynamicFeatureManager, times(1)).getVariantAsync("exitingVariant", Integer.class); + } + +} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/FeatureHandlerTest.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/FeatureHandlerTest.java similarity index 88% rename from sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/FeatureHandlerTest.java rename to sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/FeatureHandlerTest.java index fb023899fbc97..7db1a07f70726 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/FeatureHandlerTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/FeatureHandlerTest.java @@ -2,28 +2,38 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.manager; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.web.method.HandlerMethod; -import reactor.core.publisher.Mono; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.UndeclaredThrowableException; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.web.method.HandlerMethod; + +import reactor.core.publisher.Mono; /** * Unit test for simple App. */ -@RunWith(MockitoJUnitRunner.class) public class FeatureHandlerTest { @InjectMocks @@ -49,6 +59,12 @@ public class FeatureHandlerTest { @Mock FeatureHandler featureHandler2; + + @BeforeEach + public void setup(TestInfo testInfo) { + MockitoAnnotations.openMocks(this); + } + @Test public void preHandleNotHandler() { diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/FeatureManagerSnapshotTest.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/FeatureManagerSnapshotTest.java similarity index 91% rename from sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/FeatureManagerSnapshotTest.java rename to sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/FeatureManagerSnapshotTest.java index c76e987d47fbe..67a2cdafb492a 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/FeatureManagerSnapshotTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/FeatureManagerSnapshotTest.java @@ -2,7 +2,8 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.manager; -import static org.junit.Assert.assertTrue; + +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -11,19 +12,16 @@ import javax.servlet.http.HttpServletRequest; -import org.junit.Test; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; -import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import org.mockito.junit.MockitoJUnitRunner; import reactor.core.publisher.Mono; -@RunWith(MockitoJUnitRunner.class) public class FeatureManagerSnapshotTest { @Mock diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/CHANGELOG.md b/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md similarity index 88% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/CHANGELOG.md rename to sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md index 31c25b1a60cdd..e63a71591959e 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-feature-management/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 2.11.0-beta.1 (Unreleased) +## 4.0.0-beta.3 (Unreleased) ### Features Added @@ -10,12 +10,12 @@ ### Other Changes -## 2.10.0 (2023-01-18) -Upgrade Spring Boot dependencies version to 2.7.7 and Spring Cloud dependencies version to 2021.0.5 +## 4.0.0-beta.2 (2022-10-06) -## 2.9.0 (2022-11-24) -- This release is compatible with Spring Boot 2.5.0-2.5.14, 2.6.0-2.6.13, 2.7.0-2.7.5. (Note: 2.5.x (x>14), 2.6.y (y>13) and 2.7.z (z>5) should be supported, but they aren't tested with this release.) -- This release is compatible with Spring Cloud 2020.0.3-2020.0.6, 2021.0.0-2021.0.5. (Note: 2020.0.x (x>6) and 2021.0.y (y>5) should be supported, but they aren't tested with this release.) +- Dynamic Feature release with: + - Geo-replication + - Spring Boot 2.5.0-2.5.14, 2.6.0-2.6.11, 2.7.0-2.7.3. (Note: 2.5.x (x>14), 2.6.y (y>11) and 2.7.z (z>3) should be supported, but they aren't tested with this release.) + - Spring Cloud 2020.0.3-2020.0.6, 2021.0.0-2021.0.3. (Note: 2020.0.x (x>6) and 2021.0.y (y>3) should be supported, but they aren't tested with this release.) ## 2.8.0 (2022-09-22) - This release is compatible with Spring Boot 2.5.0-2.5.14, 2.6.0-2.6.11, 2.7.0-2.7.3. (Note: 2.5.x (x>14), 2.6.y (y>11) and 2.7.z (z>3) should be supported, but they aren't tested with this release.) @@ -31,6 +31,11 @@ Upgrade Spring Boot dependencies version to 2.7.7 and Spring Cloud dependencies ### Dependency Upgrades - Upgrade azure-sdk's version to latest released version. +## 4.0.0-beta.1 (2022-06-21) + +- Adds Support for Dynamic Features. +- Updated to use both the old and new Feature Management schema. + ## 2.6.0 (2022-05-24) - This release is compatible with Spring Boot 2.5.0-2.5.13, 2.6.0-2.6.7. (Note: 2.5.x (x>13) and 2.6.y (y>7) should be supported, but they aren't tested with this release.) - This release is compatible with Spring Cloud 2020.0.3-2020.0.5, 2021.0.0-2021.0.2. (Note: 2020.0.x (x>5) and 2021.0.y (y>2) should be supported, but they aren't tested with this release.) diff --git a/sdk/spring/spring-cloud-azure-feature-management/README.md b/sdk/spring/spring-cloud-azure-feature-management/README.md new file mode 100644 index 0000000000000..412c116dae643 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/README.md @@ -0,0 +1,484 @@ +# Spring Cloud for Azure feature management client library for Java + +## Feature Management + +Feature flags provide a way for Spring Boot applications to turn features on or off dynamically. Developers can use feature flags in simple use cases like conditional statement to more advanced scenarios like conditionally adding routes. Feature Flags are not dependent of any spring-cloud-azure dependencies, but may be used in conjunction with spring-cloud-azure-appconfiguration-config. + +Here are some of the benefits of using this library: + +* A common convention for feature management +* Low barrier-to-entry + * Supports application.yml file feature flag setup +* Feature Flag lifetime management + * Configuration values can change in real-time, feature flags can be consistent across the entire request + +## Feature Flags + +Feature flags are composed of two parts, a name and a list of feature-filters that are used to turn the feature on. + +## Feature Filters + +Feature filters define a scenario for when a feature should be enabled. When a feature is evaluated for whether it is on or off, its list of feature-filters are traversed until one of the filters decides the feature should be enabled. At this point the feature is considered enabled and traversal through the feature filters stops. If no feature filter indicates that the feature should be enabled, then it will be considered disabled. + +As an example, a Microsoft Edge browser feature filter could be designed. This feature filter would activate any features it is attached to as long as an HTTP request is coming from Microsoft Edge. + +## Registration + +The Spring Configuration system is used to determine the state of feature flags. Any system can be used to have them read in, such as application.yml, spring-cloud-azure-appconfiguration-config and more. + +## Feature Flag Declaration + +The feature management library supports application.yml or bootstrap.yml as a feature flag source. Below we have an example of the format used to set up feature flags in a application.yml file. + +```yaml +feature-management: + feature-flags: + feature-t: false + feature-u: + enabled-for: + - + name: Random + feature-v: + enabled-for: + - + name: TimeWindowFilter + parameters: + time-window-filter-setting-start: "Wed, 01 May 2019 13:59:59 GMT" + time-window-filter-setting-end: "Mon, 01 July 2019 00:00:00 GMT" + feature-w: + evaluate: false + enabled-for: + - + name: AlwaysOnFilter +``` + +The `feature-management` section of the YAML document is used by convention to load feature flags. In the section above, we see that we have provided three different features. Features define their filters using the `enabled-for` property. We can see that feature `feature-t` is set to false with no filters set. `feature-t` will always return false, this can also be done for true. `feature-u` which has only one feature filter `Random` which does not require any configuration so it only has the name property. `feature-v` it specifies a feature filter named `TimeWindow`. This is an example of a configurable feature filter. We can see in the example that the filter has a parameter's property. This is used to configure the filter. In this case, the start and end times for the feature to be active are configured. + +The `AlwaysOnFilter` is a Filter that always evaluates as `true`. This filter can be used to turn this feature flag on, without removing the other feature filters. The `evaluate` field is used to stop the evaluation of the feature filters, and results in the feature flag to always return `false`. + +### Supported properties + +Name | Description | Required | Default +---|---|---|--- +spring.cloud.azure.feature.management.fail-fast | Whether throw RuntimeException or not when exception occurs | No | true + +## Consumption + +The simplest use case for feature flags is to do a conditional check for whether a feature is enabled to take different paths in code. The use cases grow when additional using spring-cloud-azure-feature-flag-web to manage web based features. + +### Feature Check + +The basic form of feature management is checking if a feature is enabled and then performing actions based on the result. This is done through the autowiring `FeatureManager` and calling it's `isEnabledAsync` method. + +```java +@Autowired +FeatureManager featureManager; + +if(featureManager.isEnabledAsync("feature-t").block()) { + // Do Something +} +``` + +`FeatureManager` can also be accessed by `@Component` classes. + +### Controllers + +When using the Feature Management Web library you can require that a given feature is enabled in order to execute. This can be done by using the `@FeatureOn` annotation. + +```java +@GetMapping("/featureT") +@FeatureGate(feature = "feature-t") +@ResponseBody +public String featureT() { + ... +} +``` + +The `featureT` endpoint can only be accessed if "feature-t" is enabled. + +### Disabled Action Handling + +When a controller is blocked because the feature it specifies is disabled, `IDisabledFeaturesHandler` will be invoked. By default, a HTTP 404 is returned. This can be overridden using implementing `IDisabledFeaturesHandler`. + +```java +@Component +public class DisabledFeaturesHandler implements IDisabledFeaturesHandler{ + + @Override + public HttpServletResponse handleDisabledFeatures(HttpServletRequest request, HttpServletResponse response) { + ... + return response; + } + +} +``` + +### Routing + +Certain routes may expose application capabilites that are gated by features. These routes can redirected if a feature is disabled to another endpoint. + +```java +@GetMapping("/featureT") +@FeatureGate(feature = "feature-t" fallback= "/oldEndpoint") +@ResponseBody +public String featureT() { + ... +} + +@GetMapping("/oldEndpoint") +@ResponseBody +public String oldEndpoint() { + ... +} +``` + +## Implementing a Feature Filter + +Creating a feature filter provides a way to enable features based on criteria that you define. To implement a feature filter, the `FeatureFilter` interface must be implemented. `FeatureFilter` has a single method `evaluate`. When a feature specifies that it can be enabled with a feature filter, the `evaluate` method is called. If `evaluate` returns `true` it means the feature should be enabled. If `false` it will continue evaluating the Feature's filters until one returns true. If all return `false` then the feature is off. + +Feature filters are found by being defined as `@Component` where there name matches the expected filter defined in the configuration. + +```java +@Component("Random") +public class Random implements FeatureFilter { + + @Override + public boolean evaluate(FeatureFilterEvaluationContext context) { + double chance = Double.valueOf((String) context.getParameters().get("chance")); + return Math.random() > chance/100; + } + +} +``` + +### Parameterized Feature Filters + +Some feature filters require parameters to decide whether a feature should be turned on or not. For example a browser feature filter may turn on a feature for a certain set of browsers. It may be desired that Edge and Chrome browsers enable a feature, while FireFox does not. To do this a feature filter can be designed to expect parameters. These parameters would be specified in the feature configuration, and in code would be accessible via the `FeatureFilterEvaluationContext` parameter of `evaluate`. `FeatureFilterEvaluationContext` has a property `parameters` which is a `HashMap`. + +## Request Based Features/Snapshot + +There are scenarios which require the state of a feature to remain consistent during the lifetime of a request. The values returned from the standard `FeatureManager` may change if the configuration source which it is pulling from is updated during the request. This can be prevented by using `FeatureManagerSnapshot` and `@FeatureOn( snapshot = true )`. `FeatureManagerSnapshot` can be retrieved in the same manner as `FeatureManager`. `FeatureManagerSnapshot` calls `FeatureManager`, but it caches the first evaluated state of a feature during a request and will return the same state of a feature during its lifetime. + +## Built-In Feature Filters + +There are a few feature filters that come with the `azure-spring-cloud-feature-management` package. These feature filters are not added automatically, but can be referenced and registered as soon as the package is registered. + +Each of the built-in feature filters have their own parameters. Here is the list of feature filters along with examples. + +### PercentageFilter + +This filter provides the capability to enable a feature based on a set percentage. + +```yaml +feature-management: + feature-flags: + feature-v: + enabled-for: + - + name: PercentageFilter + parameters: + percentage-filter-setting: 50 +``` + +### TimeWindowFilter + +This filter provides the capability to enable a feature based on a time window. If only `time-window-filter-setting-end` is specified, the feature will be considered on until that time. If only start is specified, the feature will be considered on at all points after that time. If both are specified the feature will be considered valid between the two times. + +```yaml +feature-management: + feature-flags: + feature-v: + enabled-for: + - + name: TimeWindowFilter + parameters: + time-window-filter-setting-start: "Wed, 01 May 2019 13:59:59 GMT", + time-window-filter-setting-end: "Mon, 01 July 2019 00:00:00 GMT" +``` + +### TargetingFilter + +This filter provides the capability to enable a feature for a target audience. An in-depth explanation of targeting is explained in the targeting section below. The filter parameters include an audience object which describes users, groups, and a default percentage of the user base that should have access to the feature. Each group object that is listed in the target audience must also specify what percentage of the group's members should have access. If a user is specified in the users section directly, or if the user is in the included percentage of any of the group rollouts, or if the user falls into the default rollout percentage then that user will have the feature enabled. + +```yml +feature-management: + feature-flags: + target: + enabled-for: + - + name: targetingFilter + parameters: + users: + - Jeff + - Alicia + groups: + - + name: Ring0 + rolloutPercentage: 100 + - + name: Ring1 + rolloutPercentage: 100 + defaultRolloutPercentage: 50 +``` + +## Targeting + +Targeting is a feature management strategy that enables developers to progressively roll out new features to their user base. The strategy is built on the concept of targeting a set of users known as the target audience. An audience is made up of specific users, groups, and a designated percentage of the entire user base. The groups that are included in the audience can be broken down further into percentages of their total members. + +The following steps demonstrate an example of a progressive rollout for a new 'Beta' feature: + +1. Individual users Jeff and Alicia are granted access to the Beta +1. Another user, Mark, asks to opt-in and is included. +1. Twenty percent of a group known as "Ring1" users are included in the Beta. +1. The number of "Ring1" users included in the beta is bumped up to 100 percent. +1. Five percent of the user base is included in the beta. +1. The rollout percentage is bumped up to 100 percent and the feature is completely rolled out. +1. This strategy for rolling out a feature is built in to the library through the included TargetingFilter feature filter. + +### Targeting in an Application + +An example web application that uses the targeting feature filter is available in the [example project][example_project]. + +To begin using the `TargetingFilter` in an application it must be added as a `@Bean` like any other Feature Filter. `TargetingFilter` relies on another `@Bean` to be added to the application, `ITargetingContextAccessor`. The `ITargetingContextAccessor` allows for defining the current `TargetingContext` to be used for defining the current user id and groups. An example of this is: + +```java +public class TargetingContextAccessor implements ITargetingContextAccessor { + + @Override + public Mono getContextAsync() { + TargetingContext context = new TargetingContext(); + context.setUserId("Jeff"); + ArrayList groups = new ArrayList(); + groups.add("Ring0"); + context.setGroups(groups); + return Mono.just(context); + } + +} +``` + +### Targeting Evaluation Options + +Options are available to customize how targeting evaluation is performed across a given `TargetingFilter`. An optional parameter `TargetingEvaluationOptions` can be set during `TargetingFilter` creation. + +```java + @Bean + public TargetingFilter targetingFilter(ITargetingContextAccessor contextAccessor) { + return new TargetingFilter(contextAccessor, new TargetingEvaluationOptions().setIgnoreCase(true)); + } +``` + +## Dynamic Features + +When new features are being added to an application there may come a time when a feature has multiple different proposed design options. A common pattern when this happens is to do some form of A/B testing. That is, provide a different version of the feature to different segments of the user base, and judge off user interaction which is better. The dynamic feature functionality contained in this library aims to proivde a simplistic, standardized method for developers to perform this form of A/B testing. + +In the scenario above, the different proposals for the design of a feature are referred to as variants of the feature. The feature itself is referred to as a dynamic feature. The variants of a dynamic feature can have types ranging from object, to string, to integer and so on. There is no limit to the amount of variants a dynamic feature may have. A developer is free to choose what type should be returned when a variant of a dynamic feature is requested. They are also free to choose how many variants are available to select from. + +Each variant of a dynamic feature is associated with a different configuration of the feature. Additionally, each variant of a dynamic feature contains information describing under what circumstances the variant should be used. + +### Consumption + +Dynamic Features are accessible through `DynamicFeatureManager`. + +The dynamic feature manager performs a resolution process that takes the name of a feature and returns a strongly typed value to represent the variant's value. + +The following steps are performed during the retrieval of a dynamic feature's variant + +1. Lookup the configuration of the specified dynamic feature to find the registered variants +1. Assign one of the registered variants to be used. +1. Resolve typed value based off of the assigned variant. + +The Dynamic Feature Manager is made available by using `@Autwired` on `DynamicFeatureManager` and calling it's `getVariantAsync` method. In addition any required feature variant assigners need to be generated as `@Component`, such as the `TargetingEvaluator`. + +**NOTE:** `TargetingEvaluator` extends `TargetingFilter` so it can be used for both at the same time. + +### Usage Example + +One possible example of when variants may be used is in a web application when there is a desire to test different visuals. In the following examples a mock of how one might assign different variants of a web page background to their users is shown. + +```java +@Autowired +private DynamicFeatureManager dynamicFeatureManager; + +... + +// +// Modify view based off multiple possible variants +model.setBackgroundUrl(dynamicFeatureManager.GetVariantAsync("HomeBackground", String.class).block()); +``` + +### Dynamic Feature Declaration + +Dynamic features can be configured in a configuration file similarly to feature flags. Instead of being defined in the `FeatureManagement.FeatureFlags` section, they are defined in the `FeatureManagement.DynamicFeatures` section. Additionally, dynamic features have the following properties. + +* Assigner: The assigner that should be used to select which variant should be used any time this feature is accessed. +* Variants: The different variants of the dynamic feature. + * Name: The name of the variant. + * Default: Whether the variant should be used if no variant could be explicitly assigned. One and only one default variant is required. + * ConfigurationReference: A reference to the configuration of the variant to be used as typed options in the application. + * AssignmentParameters: The parameters used in the assignment process to determine if this variant should be used. + +An example of a dynamic feature named "ShoppingCart" is shown below. + +```yml +feature-management: + dynamic-features: + ShoppingCart: + assigner: Microsoft.Targeting + variants: + - default: true + name: Big + configuration-reference: ShoppingCart.Big + assignment-parameters: + audience: + users: + - Alec + groups: [] + - name: Small + configuration-reference: ShoppingCart.Small + assignment-parameters: + audience: + users: [] + groups: + - name: Ring1 + rollout-percentage: 50 + default-rollout-percentage: 30 +feature-variants: + ShoppingCart: + Big: + Size: 400 + Color: green + Small: + Size: 150 + Color: gray +``` + +In the example above we see the declaration of a dynamic feature in a json configuration file. The dynamic feature is defined in the `feature-management.dynamic-features` section of configuration. The name of this dynamic feature is `ShoppingCart`. A dynamic feature must declare a feature variant assigner that should be used to select a variant when requested. In this case the built-in `Microsoft.Targeting` feature variant assigner is used. The dynamic feature has two different variants that are available to the application. One variant is named `Big` and the other is named `Small`. Each variant contains a configuration reference denoted by the `configuration-reference` property. The configuration reference is a pointer to a section of application configuration that contains the options that should be used for that variant. The variant also contains assignment parameters denoted by the `assignment-parameters` property. The assignment parameters are used by the assigner associated with the dynamic feature. The assigner reads the assignment parameters at run time when a variant of the dynamic feature is requested to choose which variant should be returned. + +An application that is configured with this `ShoppingCart` dynamic feature may request the value of a variant of the feature at runtime through the use of `DynamicFeatureManager.getVariantAsync`. The dynamic feature uses targeting for [variant assignment](#feature-variant-assignment) so each of the variants' assignment parameters specify a target audience that should receive the variant. For a walkthrough of how the targeting assigner would choose a variant in this scenario reference the [Microsoft.Targeting Assigner](#microsofttargeting-feature-variant-assigner) section. When the feature manager chooses one of the variants it resolves the value of the variant by resolving the configuration reference declared in the variant. The example above includes the configuration that is referenced by the `configuration-reference` of each variant. + +### Feature Variant Assigners + +A feature variant assigner is a component that uses contextual information within an application to decide which feature variant should be chosen when a variant of a dynamic feature is requested. + +### Feature Variant Assignment + +When requesting the value of a dynamic feature, the feature manager needs to determine which variant to use. The act of choosing which of the variants to be used is called "assignment." A built-in method of assignment allows the variants of a dynamic feature to be assigned to segments of an application's audience. This is the same [targeting](#microsofttargeting-feature-variant-assigner) strategy used by the targeting feature filter. + +To perform assignments, the feature manager uses components known as feature variant assigners. Feature variant assigners choose which of the variants of a dynamic feature should be assigned when a dynamic feature is requested. Each variant of a dynamic feature defines assignment parameters so that when an assigner is invoked, the assigner can tell under which conditions each variant should be selected. It is possible that an assigner is unable to choose between the list of available variants based on the configured assignment parameters. In this case, the feature manager chooses the **default variant**. The default variant is a variant that is marked explicitly as the default. It is required to have a default variant when configuring a dynamic feature in order to handle the possibility that an assigner is not able to select a variant of a dynamic feature. + +### Custom Assignment + +There may come a time when custom criteria is needed to decide which variant of a feature should be assigned when a feature is referenced. This is made possible by an extensibility model that allows the act of assignment to be overridden. Every feature registered in the feature management system that uses feature variants specifies what assigner should be used to choose a variant. + +```java +public interface IFeatureVariantAssigner extends IFeatureVariantAssignerMetadata { + + /** + * Assign a variant of a dynamic feature to be used based off of customized criteria. + * @param featureDefinition A variant assignment context that contains information needed to assign a variant for a + * dynamic feature. + * @return The variant that should be assigned for a given dynamic feature. + */ + public Mono assignVariantAsync(FeatureDefinition featureDefinition); + +} +``` + +### Built-In Feature Variant Assigners + +There is a built-in feature variant assigner that uses targeting. It comes with the `azure-spring-cloud-feature-management` package. This assigner is not added automatically, but it can be referenced and configured as a `@Component`. + +#### Microsoft.Targeting Feature Variant Assigner + +This feature variant assigner provides the capability to assign the variants of a dynamic feature to targeted audiences. An in-depth explanation of targeting is explained in the [targeting](#targeting) section. + +The assignment parameters used by the targeting feature variant assigner include an audience object which describes the user base that should receive the associated variant. The audience is made of users, groups, and a percentage of the entire user base. Each group object that is listed in the target audience is required to specify what percentage of the group's members should have receive the variant. If a user is specified in the users section directly, or if the user is in the included percentage of any of the group rollouts, or if the user falls into the default rollout percentage then that user will receive the associated variant. + +```yml +ShoppingCart: + assigner: Microsoft.Targeting + variants: + - default: true + name: Big + configuration-reference: ShoppingCart.Big + assignment-parameters: + audience: + users: + - + Alec + groups: + - + name: Ring0 + rollout-percentage: 100 + - + name: Ring1 + rollout-percentage: 50 + - name: Small + configuration-reference: ShoppingCart.Small + assignment-parameters: + audience: + users: + - Susan + groups: + - + name: Ring1 + rollout-percentage: 50 + default-rollout-percentage: 80 +``` + +Based on the configured audiences for the variants included in this feature, if the application is executed under the context of a user named `Alec` then the value of the `Big` variant will be returned. If the application is executing under the context of a user named `Susan` then the value of the `Small` variant will be returned. If a user match does not occur, then group matches are evaluated. If the application is executed under the context of a user in the group `Ring0` then the `Big` variant will be returned. If the user's group is `Ring1` instead, then the user has a 50% chance of being assigned to `Small`. If there is no user match nor group match, then the default rollout percentage is used. In this case, 80% of unmatched users will get the `Small` variant, leaving the other 20% to get the `Big` variant since it is marked as the Default. + +When using the targeting feature variant assigner, make sure to register it as well as an implementation of [ITargetingContextAccessor](#itargetingcontextaccessor). + +```java +@Component +@RequestScope +public class TargetingContextImpl implements ITargetingContextAccessor { + + @Autowired + private HttpServletRequest request; + + @Override + public Mono getContextAsync() { + ... + } + +} +``` + +### Variant Resolution + +When a variant of a dynamic feature has been chosen, the feature management system resolves the configuration reference associated with that variant. The resolution is done through the `configuration-reference` property. In the "[Configuring a Dynamic Feature](#configuring-a-dynamic-feature)" section we see a dynamic feature named `ShoppingCart`. The first variant of the feature, is named "Big", and is being referenced in the feature variant as `ShoppingCart.Big` in the configuration reference. The referenced section is shown below. + +```yml +feature-variants: + ShoppingCart: + Big: + size: 400 + color: "green" +``` + +In this case, there is a `@ConfigurationProperties` that implements `IDynamicFeatureProperties` + +```java +@ConfigurationProperties(prefix = "feature-variants") +public class ApplicationProperties implements IDynamicFeatureProperties { + + private Map shoppingCart; + + public Map getShoppingCart() { + return shoppingCart; + } + + public void setShoppingCart(Map shoppingCart) { + this.shoppingCart = shoppingCart; + } + +} +``` + +`IDynamicFeatureProperties` flags `@ConfigurationProperties` as containing `FeatureVariants`. Multiple `@ConfigurationProperties` can have `IDynamicFeatureProperties`. The feature management system resolves the configuration reference by accessing the `@ConfigurationProperties` and getting the variant from the `Map`. + + +[example_project]: https://github.com/Azure-Samples/azure-spring-boot-samples/tree/tag_azure-spring-boot_3.6.0/appconfiguration/feature-management-web-sample diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/pom.xml b/sdk/spring/spring-cloud-azure-feature-management/pom.xml similarity index 64% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/pom.xml rename to sdk/spring/spring-cloud-azure-feature-management/pom.xml index 0542e305c70c2..b7647e7c7e450 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/pom.xml +++ b/sdk/spring/spring-cloud-azure-feature-management/pom.xml @@ -1,6 +1,4 @@ - + com.azure azure-client-sdk-parent @@ -10,25 +8,34 @@ 4.0.0 com.azure.spring - azure-spring-cloud-feature-management - 2.11.0-beta.1 - Azure Spring Cloud Feature Management + spring-cloud-azure-feature-management + 4.0.0-beta.3 + Spring Cloud Azure Feature Management Adds Feature Management into Spring - - true - + + scm:git:git@github.com:Azure/azure-sdk-for-java.git + scm:git:ssh://git@github.com:Azure/azure-sdk-for-java.git + https://github.com/Azure/azure-sdk-for-java + + + + + microsoft + Microsoft Corporation + + org.springframework spring-context - 5.3.25 + 5.3.23 org.springframework.boot spring-boot-starter - 2.7.8 + 2.7.4 com.fasterxml.jackson.core @@ -38,17 +45,17 @@ com.fasterxml.jackson.core jackson-databind - 2.13.4.2 + 2.13.4 io.projectreactor.netty reactor-netty - 1.0.27 + 1.0.23 org.springframework.boot spring-boot-starter-test - 2.7.8 + 2.7.4 test @@ -63,10 +70,10 @@ com.fasterxml.jackson.core:jackson-annotations:[2.13.4] - com.fasterxml.jackson.core:jackson-databind:[2.13.4.2] - io.projectreactor.netty:reactor-netty:[1.0.27] - org.springframework.boot:spring-boot-starter:[2.7.8] - org.springframework:spring-context:[5.3.25] + com.fasterxml.jackson.core:jackson-databind:[2.13.4] + io.projectreactor.netty:reactor-netty:[1.0.23] + org.springframework.boot:spring-boot-starter:[2.7.4] + org.springframework:spring-context:[5.3.23] @@ -74,4 +81,4 @@ - + \ No newline at end of file diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureException.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureException.java new file mode 100644 index 0000000000000..c32d50f561262 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureException.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager; + +/** + * Error thrown when an issue is found while generating a Feature Variant. + */ +public final class DynamicFeatureException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * Creates a new instance of the DynamicFeatureException + * + * @param message the error message. + * @param cause original issue caught + */ + DynamicFeatureException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Creates a new instance of the DynamicFeatureException + * + * @param message the error message. + */ + DynamicFeatureException(String message) { + super(message); + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManager.java new file mode 100644 index 0000000000000..c2f8a220dc07a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManager.java @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Optional; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.ApplicationContext; +import org.springframework.util.StringUtils; + +import com.azure.spring.cloud.feature.manager.implementation.FeatureManagementProperties; +import com.azure.spring.cloud.feature.manager.implementation.models.DynamicFeature; +import com.azure.spring.cloud.feature.manager.models.FeatureDefinition; +import com.azure.spring.cloud.feature.manager.models.FeatureVariant; +import com.azure.spring.cloud.feature.manager.models.IFeatureVariantAssigner; + +import reactor.core.publisher.Mono; + +/** + * Holds information on Feature Management properties and can check if a given feature is enabled. + */ +public class DynamicFeatureManager { + + private transient ApplicationContext context; + + /** + * ConfigurationProperties for accessing the different types of feature variants. + */ + private final ObjectProvider propertiesProvider; + + private final FeatureManagementProperties featureManagementConfigurations; + + /** + * Creates Dynamic Feature Manager + * + * @param context ApplicationContext + * @param propertiesProvider Object Provider for accessing client IDynamicFeatureProperties + * @param featureManagementConfigurations Configuration Properties for Feature Flags + */ + DynamicFeatureManager(ApplicationContext context, + ObjectProvider propertiesProvider, + FeatureManagementProperties featureManagementConfigurations) { + this.context = context; + this.propertiesProvider = propertiesProvider; + this.featureManagementConfigurations = featureManagementConfigurations; + } + + /** + * Returns a feature variant of the type given. + * + * @param Type of the feature that will be returned. + * @param variantName name of the feature being checked. + * @param returnClass Type of the feature being checked. + * @return variant of the provided type + */ + public Mono getVariantAsync(String variantName, Class returnClass) { + return generateVariant(variantName, returnClass); + } + + @SuppressWarnings("unchecked") + private Mono generateVariant(String featureName, Class type) { + + if (!StringUtils.hasText(featureName)) { + throw new IllegalArgumentException("Feature Variant name can not be empty or null."); + } + + if (!featureManagementConfigurations.getDynamicFeatures().containsKey(featureName)) { + throw new FeatureManagementException("The Dynamic Feature " + featureName + " can not be found."); + } + + DynamicFeature dynamicFeature = featureManagementConfigurations.getDynamicFeatures().get(featureName); + + FeatureDefinition featureDefinition = new FeatureDefinition(featureName, dynamicFeature.getAssigner(), + dynamicFeature.getVariants()); + + validateDynamicFeature(featureDefinition, featureName); + + try { + IFeatureVariantAssigner assigner = (IFeatureVariantAssigner) context + .getBean(featureDefinition.getAssigner()); + return (Mono) assigner.assignVariantAsync(featureDefinition).map(this::assignVariant); + } catch (NoSuchBeanDefinitionException e) { + throw new FeatureManagementException("The feature variant assigner " + featureDefinition.getAssigner() + + " specified for feature " + featureName + " was not found."); + } + } + + @SuppressWarnings("unchecked") + private T assignVariant(FeatureVariant variant) { + String reference = variant.getConfigurationReference(); + + String[] parts = reference.split("\\."); + + String methodName = "get" + parts[0]; + Method method = null; + Map variantMap = null; + + Optional variantProperties = propertiesProvider.stream().filter(properties -> { + try { + properties.getClass().getMethod(methodName); + return true; + } catch (NoSuchMethodException | SecurityException e) { + return false; + } + }).findFirst(); + + if (!variantProperties.isPresent()) { + String message = "Failed to load " + methodName + ". No ConfigurationProperties where found containing it." + + ". Make sure it exists and is publicly accessible."; + throw new DynamicFeatureException(message); + } + + // Dynamically Accesses @ConfigurationProperties and finds the matching method. + try { + method = variantProperties.get().getClass().getMethod(methodName); + } catch (NoSuchMethodException | SecurityException e) { + String message = "Failed to load " + methodName + " in " + variantProperties.getClass() + + ". Make sure it exists and is publicly accessible."; + throw new DynamicFeatureException(message, e); + } + // Calls method to get back an Object, this object contains multiple variants + // each has a get method. + try { + variantMap = (Map) method.invoke(variantProperties.get()); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + String message = "Failed invoking " + methodName + " in " + variantProperties.getClass() + + ". Make sure it exists and is publicly accessible."; + throw new DynamicFeatureException(message, e); + } + + return variantMap.get(parts[1]); + } + + private void validateDynamicFeature(FeatureDefinition featureDefinition, String featureName) + throws FeatureManagementException { + if (!StringUtils.hasText(featureDefinition.getAssigner())) { + throw new FeatureManagementException( + "Missing Feature Variant assigner name for the feature " + featureName); + } + + if (featureDefinition.getVariants() == null || featureDefinition.getVariants().size() == 0) { + throw new FeatureManagementException("No Variants are registered for the feature " + featureName); + } + + FeatureVariant defaultVariant = null; + + for (FeatureVariant v : featureDefinition.getVariants()) { + if (v.getDefault()) { + if (defaultVariant != null) { + throw new FeatureManagementException( + "Multiple default variants are registered for the feature " + featureName); + } + defaultVariant = v; + } + + if (!StringUtils.hasText(v.getConfigurationReference())) { + throw new FeatureManagementException("The variant " + v.getName() + " for the feature " + featureName + + " does not have a configuration reference."); + } + } + + if (defaultVariant == null) { + throw new FeatureManagementException("A default variant cannot be found for the feature " + featureName); + } + } +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java new file mode 100644 index 0000000000000..023446698d462 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.azure.spring.cloud.feature.manager.implementation.FeatureManagementConfigProperties; +import com.azure.spring.cloud.feature.manager.implementation.FeatureManagementProperties; + +/** + * Configuration for setting up FeatureManager + */ +@Configuration +@EnableConfigurationProperties({ FeatureManagementConfigProperties.class, FeatureManagementProperties.class }) +class FeatureManagementConfiguration { + + /** + * Creates Feature Manager + * + * @param context ApplicationContext + * @param featureManagementConfigurations Configuration Properties for Feature Flags + * @param properties Feature Management configuration properties + * @return FeatureManager + */ + @Bean + FeatureManager featureManager(ApplicationContext context, + FeatureManagementProperties featureManagementConfigurations, FeatureManagementConfigProperties properties) { + return new FeatureManager(context, featureManagementConfigurations, properties); + } + + /** + * Creates Dynamic Feature Manager + * + * @param context ApplicationContext + * @param propertiesProvider Object Provider for accessing client IDynamicFeatureProperties + * @param featureManagementConfigurations Configuration Properties for Feature Flags + * @return DynamicFeatureManager + */ + @Bean + DynamicFeatureManager dynamicFeatureManager(ApplicationContext context, + ObjectProvider propertiesProvider, + FeatureManagementProperties featureManagementConfigurations) { + return new DynamicFeatureManager(context, propertiesProvider, featureManagementConfigurations); + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementException.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementException.java new file mode 100644 index 0000000000000..b8606f2628f0f --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementException.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager; + +/** + * This class defines a custom exception type for when an expected Filter is not found when checking if a Feature is + * enabled. A FilterNotFoundException is only thrown when failfast is enabled, which is true by default. + */ +public final class FeatureManagementException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * The error message of what caused the Feature Management Exception + */ + private final String message; + + /** + * Creates a new instance of the FilterNotFoundException + * + * @param message the error message. + */ + FeatureManagementException(String message) { + super(message); + this.message = message; + } + + /** + * Creates a new instance of the FilterNotFoundException + * + * @param message the error message. + * @param cause the original error thrown, typically of NoSuchBeanDefinitionException type. + */ + FeatureManagementException(String message, Throwable cause) { + super(message, cause); + this.message = message; + } + + @Override + public String getMessage() { + return this.message; + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManager.java new file mode 100644 index 0000000000000..723167225273d --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManager.java @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager; + +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.util.ReflectionUtils; + +import com.azure.spring.cloud.feature.manager.implementation.FeatureManagementConfigProperties; +import com.azure.spring.cloud.feature.manager.implementation.FeatureManagementProperties; +import com.azure.spring.cloud.feature.manager.implementation.models.Feature; +import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.manager.models.IFeatureFilter; + +import reactor.core.publisher.Mono; + +/** + * Holds information on Feature Management properties and can check if a given feature is enabled. + */ +public class FeatureManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(FeatureManager.class); + + private transient ApplicationContext context; + + private final FeatureManagementProperties featureManagementConfigurations; + + private transient FeatureManagementConfigProperties properties; + + /** + * Can be called to check if a feature is enabled or disabled. + * + * @param context ApplicationContext + * @param featureManagementConfigurations Configuration Properties for Feature Flags + * @param properties FeatureManagementConfigProperties + */ + FeatureManager(ApplicationContext context, FeatureManagementProperties featureManagementConfigurations, + FeatureManagementConfigProperties properties) { + this.context = context; + this.featureManagementConfigurations = featureManagementConfigurations; + this.properties = properties; + } + + /** + * Checks to see if the feature is enabled. If enabled it check each filter, once a single filter returns true it + * returns true. If no filter returns true, it returns false. If there are no filters, it returns true. If feature + * isn't found it returns false. + * + * @param feature Feature being checked. + * @return state of the feature + * @throws FilterNotFoundException file not found + */ + public Mono isEnabledAsync(String feature) throws FilterNotFoundException { + return Mono.just(checkFeatures(feature)); + } + + private boolean checkFeatures(String feature) throws FilterNotFoundException { + if (featureManagementConfigurations.getFeatureManagement() == null + || featureManagementConfigurations.getOnOff() == null) { + return false; + } + + Boolean boolFeature = featureManagementConfigurations.getOnOff().get(feature); + + if (boolFeature != null) { + return boolFeature; + } + + Feature featureItem = featureManagementConfigurations.getFeatureManagement().get(feature); + + if (featureItem == null || !featureItem.getEvaluate()) { + return false; + } + + return featureItem.getEnabledFor().values().stream().filter(Objects::nonNull) + .filter(featureFilter -> featureFilter.getName() != null) + .map(featureFilter -> isFeatureOn(featureFilter, feature)).findAny().orElse(false); + } + + private boolean isFeatureOn(FeatureFilterEvaluationContext filter, String feature) { + try { + IFeatureFilter featureFilter = (IFeatureFilter) context.getBean(filter.getName()); + filter.setFeatureName(feature); + + return featureFilter.evaluate(filter); + } catch (NoSuchBeanDefinitionException e) { + LOGGER.error("Was unable to find Filter {}. Does the class exist and set as an @Component?", + filter.getName()); + if (properties.isFailFast()) { + String message = "Fail fast is set and a Filter was unable to be found"; + ReflectionUtils.rethrowRuntimeException(new FilterNotFoundException(message, e, filter)); + } + } + return false; + } + + /** + * Returns the names of all features flags + * + * @return a set of all feature names + */ + public Set getAllFeatureNames() { + Set allFeatures = new HashSet<>(); + + allFeatures.addAll(featureManagementConfigurations.getOnOff().keySet()); + allFeatures.addAll(featureManagementConfigurations.getFeatureManagement().keySet()); + return allFeatures; + } + + /** + * @return the featureManagement + */ + Map getFeatureManagement() { + return featureManagementConfigurations.getFeatureManagement(); + } + + /** + * @return the onOff + */ + Map getOnOff() { + return featureManagementConfigurations.getOnOff(); + } + +} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FilterNotFoundException.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FilterNotFoundException.java similarity index 69% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FilterNotFoundException.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FilterNotFoundException.java index 93a212e9a81b7..bb085c6611933 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FilterNotFoundException.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FilterNotFoundException.java @@ -2,13 +2,13 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.manager; -import com.azure.spring.cloud.feature.manager.entities.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; /** * This class defines a custom exception type for when an expected Filter is not found when checking if a Feature is * enabled. A FilterNotFoundException is only thrown when failfast is enabled, which is true by default. */ -public class FilterNotFoundException extends RuntimeException { +public final class FilterNotFoundException extends RuntimeException { private static final long serialVersionUID = 1L; @@ -23,10 +23,10 @@ public class FilterNotFoundException extends RuntimeException { * Creates a new instance of the FilterNotFoundException * * @param message the error message. - * @param cause the original error thrown, typically of NoSuchBeanDefinitionException type. - * @param filter The filter context used to find the not found filter. + * @param cause the original error thrown, typically of NoSuchBeanDefinitionException type. + * @param filter The filter context used to find the not found filter. */ - public FilterNotFoundException(String message, Throwable cause, FeatureFilterEvaluationContext filter) { + FilterNotFoundException(String message, Throwable cause, FeatureFilterEvaluationContext filter) { super(message, cause); this.message = message; this.filter = filter; diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FilterParameters.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FilterParameters.java similarity index 87% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FilterParameters.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FilterParameters.java index bfa8972c4379d..ee5dbb0c4d98b 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FilterParameters.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FilterParameters.java @@ -5,7 +5,11 @@ /** * Parameters for the predefined filters. */ -public class FilterParameters { +public final class FilterParameters { + + private FilterParameters() { + + } /** * Percentage value of the returning true in the Percentage filter. diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/IDynamicFeatureProperties.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/IDynamicFeatureProperties.java new file mode 100644 index 0000000000000..429690b83021d --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/IDynamicFeatureProperties.java @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager; + +/** + * Properties interface for Azure App Configuration Dynamic Features + */ +public interface IDynamicFeatureProperties { + +} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/TargetingException.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/TargetingException.java similarity index 84% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/TargetingException.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/TargetingException.java index 0620a41ff8107..5d76979c93df2 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/TargetingException.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/TargetingException.java @@ -6,7 +6,7 @@ * This class defines a custom exception type for when an expected Filter is not found when checking if a Feature is * enabled. A FilterNotFoundException is only thrown when failfast is enabled, which is true by default. */ -public class TargetingException extends RuntimeException { +public final class TargetingException extends RuntimeException { private static final long serialVersionUID = 1L; @@ -22,7 +22,7 @@ public TargetingException(String message) { * Creates a new instance of the FilterNotFoundException * * @param message the error message. - * @param cause the original error thrown, typically of NoSuchBeanDefinitionException type. + * @param cause the original error thrown, typically of NoSuchBeanDefinitionException type. */ public TargetingException(String message, Throwable cause) { super(message, cause); diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/AlwaysOnFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/AlwaysOnFilter.java new file mode 100644 index 0000000000000..631f65557936b --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/AlwaysOnFilter.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager.feature.filters; + +import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.manager.models.IFeatureFilter; + +/** + * A filter that always returns true + */ +public final class AlwaysOnFilter implements IFeatureFilter { + + @Override + public boolean evaluate(FeatureFilterEvaluationContext context) { + return true; + } + +} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilter.java similarity index 84% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilter.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilter.java index 1c9f6e28af289..38030a253633d 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilter.java @@ -8,14 +8,14 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; -import com.azure.spring.cloud.feature.manager.FeatureFilter; -import com.azure.spring.cloud.feature.manager.entities.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.manager.models.IFeatureFilter; /** * A feature filter that can be used to activate a feature based on a random percentage. */ @Component("PercentageFilter") -public class PercentageFilter implements FeatureFilter { +public final class PercentageFilter implements IFeatureFilter { private static final Logger LOGGER = LoggerFactory.getLogger(PercentageFilter.class); @@ -32,7 +32,7 @@ public boolean evaluate(FeatureFilterEvaluationContext context) { boolean result = true; - if (value.equals("null") || Double.parseDouble(value) < 0) { + if ("null".equals(value) || Double.parseDouble(value) < 0) { LOGGER.warn("The {} feature filter does not have a valid {} value for feature {}.", this.getClass().getSimpleName(), PERCENTAGE_FILTER_SETTING, context.getName()); result = false; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingEvaluator.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingEvaluator.java new file mode 100644 index 0000000000000..8df9945ddc9a8 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingEvaluator.java @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager.feature.filters; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.azure.spring.cloud.feature.manager.TargetingException; +import com.azure.spring.cloud.feature.manager.implementation.targeting.Audience; +import com.azure.spring.cloud.feature.manager.implementation.targeting.GroupRollout; +import com.azure.spring.cloud.feature.manager.implementation.targeting.TargetingFilterSettings; +import com.azure.spring.cloud.feature.manager.models.FeatureDefinition; +import com.azure.spring.cloud.feature.manager.models.FeatureVariant; +import com.azure.spring.cloud.feature.manager.models.IFeatureVariantAssigner; +import com.azure.spring.cloud.feature.manager.targeting.ITargetingContextAccessor; +import com.azure.spring.cloud.feature.manager.targeting.TargetingContext; +import com.azure.spring.cloud.feature.manager.targeting.TargetingEvaluationOptions; + +import reactor.core.publisher.Mono; + +/** + * Evaluator for Dynamic Feature and Feature Filters. + */ +public final class TargetingEvaluator extends TargetingFilter implements IFeatureVariantAssigner { + + private static final Logger LOGGER = LoggerFactory.getLogger(TargetingEvaluator.class); + + /** + * `Microsoft.TargetingFilter` evaluates a user/group/overall rollout of a feature. + * + * @param contextAccessor Context for evaluating the users/groups. + */ + public TargetingEvaluator(ITargetingContextAccessor contextAccessor) { + super(contextAccessor); + } + + /** + * `Microsoft.TargetingFilter` evaluates a user/group/overall rollout of a feature. + * + * @param contextAccessor Context for evaluating the users/groups. + * @param options enables customization of the filter. + */ + public TargetingEvaluator(ITargetingContextAccessor contextAccessor, TargetingEvaluationOptions options) { + super(contextAccessor, options); + } + + @Override + @SuppressWarnings("unchecked") + public Mono assignVariantAsync(FeatureDefinition featureDefinition) throws TargetingException { + if (contextAccessor == null) { + LOGGER.warn("No Context Accessor set for TargetingEvaluator."); + return Mono.justOrEmpty(null); + } + + TargetingContext targetingContext = contextAccessor.getContextAsync().block(); + + if (targetingContext == null) { + LOGGER.warn("No targeting context available for targeting evaluation."); + return Mono.justOrEmpty(null); + } + + Map assignments = new HashMap<>(); + + List variants = featureDefinition.getVariants(); + + FeatureVariant defaultVariant = null; + + validateVariantSettings(variants); + + Map totalGroupPercentages = new HashMap<>(); + double totalDefaultPercentage = 0; + + for (FeatureVariant variant : variants) { + + LinkedHashMap parameters = variant.getAssignmentParameters(); + + if (parameters != null) { + Object audienceObject = parameters.get(AUDIENCE); + if (audienceObject != null) { + parameters = (LinkedHashMap) audienceObject; + } + + this.updateValueFromMapToList(parameters, USERS); + + assignments.put(variant, OBJECT_MAPPER.convertValue(parameters, Audience.class)); + } + + if (variant.getDefault()) { + defaultVariant = variant; + } + + } + + // First, we need to check if a users is assigned to a variant. + for (Entry assignment : assignments.entrySet()) { + Audience audience = assignment.getValue(); + if (targetingContext.getUserId() != null && audience.getUsers() != null && audience.getUsers().stream() + .anyMatch(user -> compareStrings(targetingContext.getUserId(), user))) { + return Mono.just(assignment.getKey()); + } + } + + // Second, is the user part of of a group and in the groups rollout percentage + for (Entry assignment : assignments.entrySet()) { + Audience audience = assignment.getValue(); + if (targetingContext.getGroups() != null && audience.getGroups() != null) { + for (String group : targetingContext.getGroups()) { + Optional groupRollout = audience.getGroups().stream() + .filter(g -> compareStrings(g.getName(), group)).findFirst(); + + if (groupRollout.isPresent()) { + String audienceContextId = targetingContext.getUserId() + "\n" + featureDefinition.getName() + + "\n" + group; + + double chance = totalGroupPercentages.getOrDefault(group, (double) 0); + + if (isTargetedPercentage(audienceContextId) < groupRollout.get().getRolloutPercentage() + + chance) { + return Mono.just(assignment.getKey()); + } + totalGroupPercentages.put(group, chance + groupRollout.get().getRolloutPercentage()); + } + } + } + } + + // Third, is the user part of the default rollout + for (Entry assignment : assignments.entrySet()) { + Audience audience = assignment.getValue(); + String defaultContextId = targetingContext.getUserId() + "\n" + featureDefinition.getName(); + + if (isTargetedPercentage(defaultContextId) < audience.getDefaultRolloutPercentage() + + totalDefaultPercentage) { + return Mono.just(assignment.getKey()); + } + totalDefaultPercentage += audience.getDefaultRolloutPercentage(); + } + + // Defautl is returned when the user needs to be assigned the default variant + return Mono.just(defaultVariant); + } + + /** + * Validates the settings of the variant. + * + * @param variantSettings variant settings + * @throws TargetingException thrown when percentage range is greater than 100 + */ + @SuppressWarnings("unchecked") + protected void validateVariantSettings(List variantSettings) throws TargetingException { + Map groupUsed = new HashMap<>(); + + for (FeatureVariant variant : variantSettings) { + TargetingFilterSettings settings = new TargetingFilterSettings(); + LinkedHashMap parameters = variant.getAssignmentParameters(); + + if (parameters != null) { + Object audienceObject = parameters.get(AUDIENCE); + if (audienceObject != null) { + parameters = (LinkedHashMap) audienceObject; + } + + this.updateValueFromMapToList(parameters, USERS); + updateValueFromMapToList(parameters, GROUPS); + + settings.setAudience(OBJECT_MAPPER.convertValue(parameters, Audience.class)); + } + + validateSettings(settings); + + Audience audience = settings.getAudience(); + + List groups = audience.getGroups(); + + if (groups != null) { + + groups.forEach(groupRollout -> { + Double currentSize = groupUsed.getOrDefault(groupRollout.getName(), (double) 0); + currentSize += groupRollout.getRolloutPercentage(); + if (currentSize > 100) { + throw new TargetingException(groupRollout.getName() + " : " + OUT_OF_RANGE); + } + groupUsed.put(groupRollout.getName(), currentSize); + }); + } + } + } +} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingFilter.java similarity index 64% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingFilter.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingFilter.java index 553ef11010f31..d3e140997432b 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingFilter.java @@ -2,54 +2,80 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.manager.feature.filters; -import com.azure.spring.cloud.feature.manager.FeatureFilter; -import com.azure.spring.cloud.feature.manager.TargetingException; -import com.azure.spring.cloud.feature.manager.entities.FeatureFilterEvaluationContext; -import com.azure.spring.cloud.feature.manager.targeting.Audience; -import com.azure.spring.cloud.feature.manager.targeting.GroupRollout; -import com.azure.spring.cloud.feature.manager.targeting.ITargetingContextAccessor; -import com.azure.spring.cloud.feature.manager.targeting.TargetingContext; -import com.azure.spring.cloud.feature.manager.targeting.TargetingEvaluationOptions; -import com.azure.spring.cloud.feature.manager.targeting.TargetingFilterSettings; -import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.azure.spring.cloud.feature.manager.TargetingException; +import com.azure.spring.cloud.feature.manager.implementation.targeting.Audience; +import com.azure.spring.cloud.feature.manager.implementation.targeting.GroupRollout; +import com.azure.spring.cloud.feature.manager.implementation.targeting.TargetingFilterSettings; +import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.manager.models.IFeatureFilter; +import com.azure.spring.cloud.feature.manager.targeting.ITargetingContextAccessor; +import com.azure.spring.cloud.feature.manager.targeting.TargetingContext; +import com.azure.spring.cloud.feature.manager.targeting.TargetingEvaluationOptions; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; /** * `Microsoft.TargetingFilter` enables evaluating a user/group/overall rollout of a feature. */ -public class TargetingFilter implements FeatureFilter { +public class TargetingFilter implements IFeatureFilter { private static final Logger LOGGER = LoggerFactory.getLogger(TargetingFilter.class); - private static final String USERS = "users"; + /** + * users field in the filter + */ + protected static final String USERS = "users"; - private static final String GROUPS = "groups"; + /** + * groups field in the filter + */ + protected static final String GROUPS = "groups"; - private static final String AUDIENCE = "Audience"; + /** + * Audience in the filter + */ + protected static final String AUDIENCE = "Audience"; - private static final String OUT_OF_RANGE = "The value is out of the accepted range."; + /** + * Error message for when the total Audience value is greater than 100 percent. + */ + protected static final String OUT_OF_RANGE = "The value is out of the accepted range."; private static final String REQUIRED_PARAMETER = "Value cannot be null."; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() - .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); - private final ITargetingContextAccessor contextAccessor; - private final TargetingEvaluationOptions options; /** - * `Microsoft.TargetingFilter` evaluates a user/group/overall rollout of a feature. - * @param contextAccessor Context for evaluating the users/groups. + * Object Mapper for converting configurations to variants + */ + protected static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build(); + + /** + * Accessor for identifying the current user/group when evaluating + */ + protected final ITargetingContextAccessor contextAccessor; + + /** + * Options for evaluating the filter + */ + protected final TargetingEvaluationOptions options; + + /** + * Filter for targeting a user/group/percentage of users. + * @param contextAccessor Accessor for identifying the current user/group when evaluating */ public TargetingFilter(ITargetingContextAccessor contextAccessor) { this.contextAccessor = contextAccessor; @@ -96,22 +122,21 @@ public boolean evaluate(FeatureFilterEvaluationContext context) { settings.setAudience(OBJECT_MAPPER.convertValue(parameters, Audience.class)); } - tryValidateSettings(settings); + validateSettings(settings); Audience audience = settings.getAudience(); if (targetingContext.getUserId() != null && audience.getUsers() != null && audience.getUsers().stream() - .anyMatch(user -> compairStrings(targetingContext.getUserId(), user)) - ) { + .anyMatch(user -> compareStrings(targetingContext.getUserId(), user))) { return true; } if (targetingContext.getGroups() != null && audience.getGroups() != null) { for (String group : targetingContext.getGroups()) { Optional groupRollout = audience.getGroups().stream() - .filter(g -> compairStrings(g.getName(), group)).findFirst(); + .filter(g -> compareStrings(g.getName(), group)).findFirst(); if (groupRollout.isPresent()) { String audienceContextId = targetingContext.getUserId() + "\n" + context.getName() + "\n" + group; @@ -128,7 +153,13 @@ public boolean evaluate(FeatureFilterEvaluationContext context) { return isTargeted(defaultContextId, settings.getAudience().getDefaultRolloutPercentage()); } - private boolean isTargeted(String contextId, double percentage) { + /** + * Computes the percentage that the contextId falls into. + * @param contextId Id of the context being targeted + * @return the bucket value of the context id + * @throws TargetingException Unable to create hash of target context + */ + protected double isTargetedPercentage(String contextId) { byte[] hash = null; try { @@ -145,11 +176,19 @@ private boolean isTargeted(String contextId, double percentage) { ByteBuffer wrapped = ByteBuffer.wrap(hash); int contextMarker = Math.abs(wrapped.getInt()); - double contextPercentage = (contextMarker / (double) Integer.MAX_VALUE) * 100; - return contextPercentage < percentage; + return (contextMarker / (double) Integer.MAX_VALUE) * 100; + } + + private boolean isTargeted(String contextId, double percentage) { + return isTargetedPercentage(contextId) < percentage; } - private void tryValidateSettings(TargetingFilterSettings settings) { + /** + * Validates the settings of a targeting filter. + * @param settings targeting filter settings + * @throws TargetingException when a required parameter is missing or percentage value is greater than 100. + */ + void validateSettings(TargetingFilterSettings settings) { String paramName = ""; String reason = ""; @@ -183,18 +222,31 @@ private void tryValidateSettings(TargetingFilterSettings settings) { } } - private boolean compairStrings(String s1, String s2) { + /** + * Checks if two strings are equal, ignores case if configured to. + * @param s1 string to compare + * @param s2 string to compare + * @return true if the strings are equal + */ + protected boolean compareStrings(String s1, String s2) { if (options.isIgnoreCase()) { return s1.equalsIgnoreCase(s2); } return s1.equals(s2); } + /** + * Looks at the given key in the parameters and coverts it to a list if it is currently a map. Used for updating + * fields in the targeting filter. + * @param Type of object inside of parameters for the given key + * @param parameters map of generic objects + * @param key key of object int the parameters map + */ @SuppressWarnings("unchecked") - private void updateValueFromMapToList(LinkedHashMap parameters, String key) { + protected void updateValueFromMapToList(LinkedHashMap parameters, String key) { Object objectMap = parameters.get(key); if (objectMap instanceof Map) { - List toType = new ArrayList<>(((Map) objectMap).values()); + List toType = ((Map) objectMap).values().stream().collect(Collectors.toList()); parameters.put(key, toType); } } diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilter.java similarity index 92% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilter.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilter.java index f312f94a6917f..b749d71723981 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilter.java @@ -14,14 +14,14 @@ import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; -import com.azure.spring.cloud.feature.manager.FeatureFilter; -import com.azure.spring.cloud.feature.manager.entities.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.manager.models.IFeatureFilter; /** * A feature filter that can be used at activate a feature based on a time window. */ @Component("TimeWindowFilter") -public class TimeWindowFilter implements FeatureFilter { +public final class TimeWindowFilter implements IFeatureFilter { private static final Logger LOGGER = LoggerFactory.getLogger(TimeWindowFilter.class); diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/package-info.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/package-info.java new file mode 100644 index 0000000000000..fc4cd4a21bb26 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * * Package contains the built-in Feature Filters, which implement + * {@link com.azure.spring.cloud.feature.manager.models.IFeatureFilter}. + * + */ +package com.azure.spring.cloud.feature.manager.feature.filters; diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfigProperties.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementConfigProperties.java similarity index 89% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfigProperties.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementConfigProperties.java index 354390dfff4ce..a8efa2a07da59 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfigProperties.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementConfigProperties.java @@ -1,14 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager; +package com.azure.spring.cloud.feature.manager.implementation; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; /** * Feature Management configuration file properties. */ -@Validated @ConfigurationProperties(prefix = FeatureManagementConfigProperties.CONFIG_PREFIX) public class FeatureManagementConfigProperties { diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementProperties.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementProperties.java new file mode 100644 index 0000000000000..4e06803f53374 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementProperties.java @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager.implementation; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; + +import com.azure.spring.cloud.feature.manager.implementation.models.DynamicFeature; +import com.azure.spring.cloud.feature.manager.implementation.models.Feature; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Configuration Properties for Feature Management. Processes the configurations to be usable by Feature Management. + */ +@ConfigurationProperties(prefix = "feature-management") +public class FeatureManagementProperties extends HashMap { + + private static final Logger LOGGER = LoggerFactory.getLogger(FeatureManagementProperties.class); + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final long serialVersionUID = -1642032123104805346L; + + /** + * Map of all Feature Flags that use Feature Filters. + */ + private transient Map featureManagement; + + /** + * Map of all Feature Flags that are just enabled/disabled. + */ + private Map onOff; + + private transient Map dynamicFeatures; + + public FeatureManagementProperties() { + featureManagement = new HashMap<>(); + onOff = new HashMap<>(); + dynamicFeatures = new HashMap<>(); + } + + @Override + public void putAll(Map m) { + if (m == null) { + return; + } + + // Need to reset or switch between on/off to conditional doesn't work + featureManagement = new HashMap<>(); + onOff = new HashMap<>(); + dynamicFeatures = new HashMap<>(); + + Map features = removePrefixes(m, "featureManagement"); + + if (!features.isEmpty()) { + m = features; + } + + Map featureFlags = removePrefixes(m, "feature-flags"); + Map dynamicFeatures = removePrefixes(m, "dynamic-features"); + + if (featureFlags.size() > 0 || dynamicFeatures.size() > 0) { + for (String key : featureFlags.keySet()) { + addToFeatures(featureFlags, key, ""); + } + + for (String key : dynamicFeatures.keySet()) { + addToFeatures(dynamicFeatures, key, ""); + } + } else { + for (String key : m.keySet()) { + addToFeatures(m, key, ""); + } + } + } + + @SuppressWarnings("unchecked") + private Map removePrefixes(Map m, + String prefix) { + Map removedPrefix = new HashMap<>(); + if (m.containsKey(prefix)) { + removedPrefix = (Map) m.get(prefix); + } + return removedPrefix; + } + + @SuppressWarnings("unchecked") + private void addToFeatures(Map features, String key, String combined) { + Object featureValue = features.get(key); + if (!combined.isEmpty() && !combined.endsWith(".")) { + combined += "."; + } + if (featureValue instanceof Boolean) { + onOff.put(combined + key, (Boolean) featureValue); + } else { + Feature feature = null; + DynamicFeature dynamicFeature = null; + try { + feature = MAPPER.convertValue(featureValue, Feature.class); + dynamicFeature = MAPPER.convertValue(featureValue, DynamicFeature.class); + } catch (IllegalArgumentException e) { + LOGGER.error("Found invalid feature {} with value {}.", combined + key, featureValue.toString()); + } + // When coming from a file "feature.flag" is not a possible flag name + if (dynamicFeature != null && StringUtils.hasText(dynamicFeature.getAssigner()) + && dynamicFeature.getVariants().size() > 0) { + dynamicFeatures.put(key, dynamicFeature); + } else if (feature != null && feature.getEnabledFor() == null && feature.getKey() == null) { + if (LinkedHashMap.class.isAssignableFrom(featureValue.getClass())) { + features = (LinkedHashMap) featureValue; + for (String fKey : features.keySet()) { + addToFeatures(features, fKey, combined + key); + } + } + } else { + if (feature != null) { + feature.setKey(key); + featureManagement.put(key, feature); + } + } + } + } + + /** + * @return the featureManagement + */ + public Map getFeatureManagement() { + return featureManagement; + } + + /** + * @return the onOff + */ + public Map getOnOff() { + return onOff; + } + + /** + * @return the dynamicFeatures + */ + public Map getDynamicFeatures() { + return dynamicFeatures; + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/DynamicFeature.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/DynamicFeature.java new file mode 100644 index 0000000000000..10db18a85d92b --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/DynamicFeature.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager.implementation.models; + +import java.util.HashMap; +import java.util.Map; + +import com.azure.spring.cloud.feature.manager.models.FeatureVariant; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Defines the assigner and variants of a Dynamic Feature + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class DynamicFeature { + + private String assigner; + + private Map variants = new HashMap<>(); + + /** + * @return the assigner + */ + public String getAssigner() { + return assigner; + } + + /** + * @param assigner the assigner to set + */ + public void setAssigner(String assigner) { + this.assigner = assigner; + } + + /** + * @return the variants + */ + public Map getVariants() { + return variants; + } + + /** + * @param variants the variants to set + */ + public void setVariants(Map variants) { + this.variants = variants; + } +} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/entities/Feature.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/Feature.java similarity index 90% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/entities/Feature.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/Feature.java index 02697418399cd..ed80c361bd1a8 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/entities/Feature.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/Feature.java @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager.entities; +package com.azure.spring.cloud.feature.manager.implementation.models; +import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.HashMap; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/package-info.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/package-info.java new file mode 100644 index 0000000000000..885a91e581dab --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/package-info.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package containing implementation models classes for setting up Feature Flags, Feature Filters, and Feature Variants. + */ +package com.azure.spring.cloud.feature.manager.implementation.models; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java new file mode 100644 index 0000000000000..59c47a44659f8 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package containing implementation classes for initializing + * {@link com.azure.spring.cloud.feature.manager.FeatureManager} and + * {@link com.azure.spring.cloud.feature.manager.DynamicFeatureManager}. + */ +package com.azure.spring.cloud.feature.manager.implementation; diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/Audience.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/targeting/Audience.java similarity index 94% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/Audience.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/targeting/Audience.java index f514d56a2cd7e..e1470cd83e9db 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/Audience.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/targeting/Audience.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager.targeting; +package com.azure.spring.cloud.feature.manager.implementation.targeting; import java.util.List; diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/GroupRollout.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/targeting/GroupRollout.java similarity index 91% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/GroupRollout.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/targeting/GroupRollout.java index 3dfebe825e5c1..8e776f2665dd7 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/GroupRollout.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/targeting/GroupRollout.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager.targeting; +package com.azure.spring.cloud.feature.manager.implementation.targeting; /** * Properties for defining a rollout for a given group. diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingFilterSettings.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/targeting/TargetingFilterSettings.java similarity index 87% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingFilterSettings.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/targeting/TargetingFilterSettings.java index 1bfefc7d767c6..52b129b19997b 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingFilterSettings.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/targeting/TargetingFilterSettings.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager.targeting; +package com.azure.spring.cloud.feature.manager.implementation.targeting; /** * The settings that are used to configure the TargetingFilter feature filter. diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/targeting/package-info.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/targeting/package-info.java new file mode 100644 index 0000000000000..ae92722167794 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/targeting/package-info.java @@ -0,0 +1 @@ +package com.azure.spring.cloud.feature.manager.implementation.targeting; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/FeatureDefinition.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/FeatureDefinition.java new file mode 100644 index 0000000000000..a7f5f5192c5ed --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/FeatureDefinition.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager.models; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.springframework.validation.annotation.Validated; + +/** + * The definition of a dynamic feature. + */ +@Validated +public class FeatureDefinition { + + private final String name; + + private final String assigner; + + private final List variants; + + /** + * Definition of a Dynamic Feature. + * + * @param feature name of the feature + * @param assigner name of the assigner used + * @param variantMap Map of names of variants and the the FeatureVariants + */ + public FeatureDefinition(String feature, String assigner, Map variantMap) { + this.name = feature; + this.assigner = assigner; + this.variants = new ArrayList(); + + for (int i = 0; i < variantMap.size(); i++) { + variants.add(variantMap.get(String.valueOf(i))); + } + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @return the assigner + */ + public String getAssigner() { + return assigner; + } + + /** + * @return the variants + */ + public List getVariants() { + return variants; + } +} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/entities/FeatureFilterEvaluationContext.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/FeatureFilterEvaluationContext.java similarity index 91% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/entities/FeatureFilterEvaluationContext.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/FeatureFilterEvaluationContext.java index 31fa4c4999c06..888a138930117 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/entities/FeatureFilterEvaluationContext.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/FeatureFilterEvaluationContext.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager.entities; +package com.azure.spring.cloud.feature.manager.models; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -10,9 +10,8 @@ * Context passed into Feature Filters used for evaluation. */ @JsonIgnoreProperties(ignoreUnknown = true) -public class FeatureFilterEvaluationContext { +public final class FeatureFilterEvaluationContext { - @JsonProperty("name") private String name; @JsonProperty("parameters") diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/FeatureVariant.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/FeatureVariant.java new file mode 100644 index 0000000000000..1e6775d38fa0f --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/FeatureVariant.java @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager.models; + +import java.util.LinkedHashMap; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A variant of a feature. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class FeatureVariant { + + private String name; + + @JsonProperty("default") + private Boolean isDefault = false; + + @JsonProperty("configuration-reference") + @JsonAlias({"configurationReference", "configuration-reference"}) + private String configurationReference; + + @JsonProperty("assignment-parameters") + @JsonAlias({"assignment-parameters", "assignmentParameters"}) + private LinkedHashMap assignmentParameters; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the default + */ + public Boolean getDefault() { + return isDefault; + } + + /** + * @param isDefault the isDefault to set + */ + public void setDefault(Boolean isDefault) { + this.isDefault = isDefault; + } + + /** + * @return the configurationReference + */ + public String getConfigurationReference() { + return configurationReference; + } + + /** + * @param configurationReference the configurationReference to set + */ + public void setConfigurationReference(String configurationReference) { + this.configurationReference = configurationReference; + } + + /** + * @return the assignmentParameters + */ + public LinkedHashMap getAssignmentParameters() { + return assignmentParameters; + } + + /** + * @param assignmentParameters the assignmentParameters to set + */ + public void setAssignmentParameters(LinkedHashMap assignmentParameters) { + this.assignmentParameters = assignmentParameters; + } +} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/IFeatureFilter.java similarity index 79% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureFilter.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/IFeatureFilter.java index a2cf433c49e3b..c5d4d2f430ef0 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/IFeatureFilter.java @@ -1,14 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager; - -import com.azure.spring.cloud.feature.manager.entities.FeatureFilterEvaluationContext; +package com.azure.spring.cloud.feature.manager.models; /** * A Filter for Feature Management that is attached to Features. The filter needs to have @Component set to be found by * feature management. */ -public interface FeatureFilter { +public interface IFeatureFilter { /** * Evaluates if the filter is on or off. Returning true results in Feature evaluation ending and returning true. diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/IFeatureVariantAssigner.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/IFeatureVariantAssigner.java new file mode 100644 index 0000000000000..15049724ff460 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/IFeatureVariantAssigner.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager.models; + +import reactor.core.publisher.Mono; + +/** + * Provides a method to assign a variant of a dynamic feature to be used based off of custom conditions. + */ +public interface IFeatureVariantAssigner { + + /** + * Assign a variant of a dynamic feature to be used based off of customized criteria. + * @param featureDefinition A variant assignment context that contains information needed to assign a variant for a + * dynamic feature. + * @return The variant that should be assigned for a given dynamic feature. + */ + Mono assignVariantAsync(FeatureDefinition featureDefinition); + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/package-info.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/package-info.java new file mode 100644 index 0000000000000..a488773ddcff3 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/models/package-info.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package containing models classes for setting up Feature Flags, Feature Filters, and Feature Variants. + */ +package com.azure.spring.cloud.feature.manager.models; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/package-info.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/package-info.java new file mode 100644 index 0000000000000..a2005f334e0d7 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/package-info.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package contains classes for accessing and creating Feature Flags, Feature Filters, and Feature Variants. + */ +package com.azure.spring.cloud.feature.manager; diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/ITargetingContext.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/ITargetingContext.java similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/ITargetingContext.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/ITargetingContext.java diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/ITargetingContextAccessor.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/ITargetingContextAccessor.java similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/ITargetingContextAccessor.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/ITargetingContextAccessor.java diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingContext.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingContext.java similarity index 92% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingContext.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingContext.java index 7217543340b72..c4c84d8b468b4 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingContext.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingContext.java @@ -7,7 +7,7 @@ /** * Context for evaluating the `Microsoft.TargetingFilter`. */ -public class TargetingContext implements ITargetingContext { +public final class TargetingContext implements ITargetingContext { private String userId; diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingEvaluationOptions.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingEvaluationOptions.java similarity index 93% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingEvaluationOptions.java rename to sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingEvaluationOptions.java index 6512a5bb51e7c..f15a1ca1f26f7 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingEvaluationOptions.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/TargetingEvaluationOptions.java @@ -5,7 +5,7 @@ /** * Configuration options for the `Microsoft.TargetingFilter`. */ -public class TargetingEvaluationOptions { +public final class TargetingEvaluationOptions { private boolean ignoreCase; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/package-info.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/package-info.java new file mode 100644 index 0000000000000..6c5b9c9935c18 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/targeting/package-info.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * Package containing models classes for targeting user and groups with the targeting filter. + */ +package com.azure.spring.cloud.feature.manager.targeting; diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/resources/META-INF/spring.factories b/sdk/spring/spring-cloud-azure-feature-management/src/main/resources/META-INF/spring.factories similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/main/resources/META-INF/spring.factories rename to sdk/spring/spring-cloud-azure-feature-management/src/main/resources/META-INF/spring.factories diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerTest.java new file mode 100644 index 0000000000000..6320c1dda0a31 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerTest.java @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; + +import com.azure.spring.cloud.feature.manager.implementation.FeatureManagementConfigProperties; +import com.azure.spring.cloud.feature.manager.implementation.FeatureManagementProperties; +import com.azure.spring.cloud.feature.manager.implementation.models.DynamicFeature; +import com.azure.spring.cloud.feature.manager.models.FeatureDefinition; +import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.manager.models.FeatureVariant; +import com.azure.spring.cloud.feature.manager.models.IFeatureFilter; +import com.azure.spring.cloud.feature.manager.models.IFeatureVariantAssigner; +import com.azure.spring.cloud.feature.manager.testobjects.DiscountBanner; +import com.azure.spring.cloud.feature.manager.testobjects.MockableProperties; + +import reactor.core.publisher.Mono; + +/** + * Unit tests for FeatureManager. + */ +@SpringBootTest(classes = { TestConfiguration.class, SpringBootTest.class }) +public class DynamicFeatureManagerTest { + + private static final String USERS = "users"; + + private static final String GROUPS = "groups"; + + private static final String DEFAULT_ROLLOUT_PERCENTAGE = "defaultRolloutPercentage"; + + private static final LinkedHashMap EMPTY_MAP = new LinkedHashMap<>(); + + private DynamicFeatureManager featureManager; + + @Mock + private ApplicationContext context; + + @Mock + private FeatureManagementConfigProperties properties; + + @Mock + private FeatureManagementProperties featureManagementPropertiesMock; + + @Mock + private MockFilter filterMock; + + @Mock + private MockableProperties variantProperties; + + @Mock + private ObjectProvider propertiesProviderMock; + + @Mock + private Stream streamMock; + + @Mock + private Stream filterStreamMock; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + when(properties.isFailFast()).thenReturn(true); + + DiscountBanner discountBannerBig = new DiscountBanner(); + discountBannerBig.setColor("#DDD"); + discountBannerBig.setSize(400); + DiscountBanner discountBannerSmall = new DiscountBanner(); + discountBannerSmall.setColor("#999"); + discountBannerSmall.setSize(150); + + Map discountBannerMap = new HashMap<>(); + + discountBannerMap.put("Big", discountBannerBig); + discountBannerMap.put("Small", discountBannerSmall); + + when(variantProperties.getDiscountBanner()).thenReturn(discountBannerMap); + when(propertiesProviderMock.stream()).thenReturn(streamMock); + when(streamMock.filter(Mockito.any())).thenReturn(filterStreamMock); + + featureManager = new DynamicFeatureManager(context, propertiesProviderMock, featureManagementPropertiesMock); + } + + @AfterEach + public void cleanup() throws Exception { + MockitoAnnotations.openMocks(this).close(); + } + + @Test + public void getVariantAsyncDefaultBasic() { + when(context.getBean(Mockito.matches("Test.Assigner"))).thenReturn(filterMock); + + DynamicFeature dynamicFeature = new DynamicFeature(); + dynamicFeature.setAssigner("Test.Assigner"); + + Map variants = new LinkedHashMap<>(); + + variants.put("0", createFeatureVariant("DiscountBanner.Big", EMPTY_MAP, EMPTY_MAP, 100)); + variants.get("0").setDefault(true); + dynamicFeature.setVariants(variants); + + when(filterMock.assignVariantAsync(Mockito.any())).thenReturn(Mono.just(variants.get("0"))); + + Map params = new LinkedHashMap<>(); + params.put("DiscountBanner", dynamicFeature); + + Optional optionalProp = Optional.of(variantProperties); + + when(featureManagementPropertiesMock.getDynamicFeatures()).thenReturn(params); + when(filterStreamMock.findFirst()).thenReturn(optionalProp); + + DiscountBanner testObject = featureManager.getVariantAsync("DiscountBanner", DiscountBanner.class) + .block(); + + assertNotNull(testObject); + assertEquals(400, testObject.getSize()); + } + + @Test + public void getVariantNoDefault() { + DynamicFeature dynamicFeature = new DynamicFeature(); + dynamicFeature.setAssigner("Test.Assigner"); + + Map variants = new LinkedHashMap<>(); + + variants.put("0", createFeatureVariant("DiscountBanner.Big", EMPTY_MAP, EMPTY_MAP, 100)); + + dynamicFeature.setVariants(variants); + + Map params = new LinkedHashMap<>(); + params.put("DiscountBanner", dynamicFeature); + + Optional optionalProp = Optional.of(variantProperties); + + when(featureManagementPropertiesMock.getDynamicFeatures()).thenReturn(params); + when(filterStreamMock.findFirst()).thenReturn(optionalProp); + + FeatureManagementException e = assertThrows(FeatureManagementException.class, + () -> featureManager.getVariantAsync("DiscountBanner", Object.class).block()); + assertEquals("A default variant cannot be found for the feature DiscountBanner", e.getMessage()); + } + + @Test + public void getVariantAsyncNonDefault() throws FilterNotFoundException, FeatureManagementException { + FeatureVariant variant = createFeatureVariant("DiscountBanner.Small", EMPTY_MAP, EMPTY_MAP, 100); + + when(context.getBean(Mockito.matches("Test.Assigner"))).thenReturn(filterMock); + when(filterMock.assignVariantAsync(Mockito.any())).thenReturn(Mono.just(variant)); + + DynamicFeature dynamicFeature = new DynamicFeature(); + dynamicFeature.setAssigner("Test.Assigner"); + + Map variants = new LinkedHashMap<>(); + + variants.put("0", createFeatureVariant("DiscountBanner.Big", EMPTY_MAP, EMPTY_MAP, 0)); + variants.get("0").setDefault(true); + variants.put("1", variant); + dynamicFeature.setVariants(variants); + + Map params = new LinkedHashMap<>(); + params.put("DiscountBanner", dynamicFeature); + + Optional optionalProp = Optional.of(variantProperties); + + when(featureManagementPropertiesMock.getDynamicFeatures()).thenReturn(params); + when(filterStreamMock.findFirst()).thenReturn(optionalProp); + + DiscountBanner testObject = featureManager.getVariantAsync("DiscountBanner", DiscountBanner.class) + .block(); + assertNotNull(testObject); + assertEquals(150, testObject.getSize()); + } + + @Test + public void featureWithoutAName() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> featureManager.getVariantAsync("", DiscountBanner.class).block()); + assertThat(e).hasMessage("Feature Variant name can not be empty or null."); + } + + @Test + public void featureAssignerNotFound() { + FeatureVariant variant = createFeatureVariant("DiscountBanner.Small", EMPTY_MAP, EMPTY_MAP, 100); + + when(context.getBean(Mockito.matches("FeatureAssignerThatDoesntExist"))) + .thenThrow(new NoSuchBeanDefinitionException("")); + when(filterMock.assignVariantAsync(Mockito.any())).thenReturn(Mono.just(variant)); + + DynamicFeature dynamicFeature = new DynamicFeature(); + dynamicFeature.setAssigner("FeatureAssignerThatDoesntExist"); + + Map variants = new LinkedHashMap<>(); + + variants.put("0", createFeatureVariant("DiscountBanner.Big", EMPTY_MAP, EMPTY_MAP, 0)); + variants.get("0").setDefault(true); + variants.put("1", variant); + dynamicFeature.setVariants(variants); + + Map params = new LinkedHashMap<>(); + params.put("FeatureAssignerThatDoesntExist", dynamicFeature); + + Optional optionalProp = Optional.of(variantProperties); + + when(featureManagementPropertiesMock.getDynamicFeatures()).thenReturn(params); + when(filterStreamMock.findFirst()).thenReturn(optionalProp); + + FeatureManagementException e = assertThrows(FeatureManagementException.class, + () -> featureManager.getVariantAsync("FeatureAssignerThatDoesntExist", DiscountBanner.class).block()); + assertThat(e).hasMessage( + "The feature variant assigner FeatureAssignerThatDoesntExist specified for feature FeatureAssignerThatDoesntExist was not found."); + } + + class MockFilter implements IFeatureFilter, IFeatureVariantAssigner { + + @Override + public Mono assignVariantAsync(FeatureDefinition featureDefinition) { + return null; + } + + @Override + public boolean evaluate(FeatureFilterEvaluationContext context) { + return false; + } + + } + + private FeatureVariant createFeatureVariant(String variantName, LinkedHashMap users, + LinkedHashMap groups, int defautPercentage) { + return createFeatureVariant(variantName, "", users, groups, defautPercentage); + } + + private FeatureVariant createFeatureVariant(String variantName, String additionalRerence, + LinkedHashMap users, + LinkedHashMap groups, int defautPercentage) { + FeatureVariant variant = new FeatureVariant(); + variant.setName(variantName); + variant.setDefault(false); + variant.setConfigurationReference(variantName + additionalRerence); + + LinkedHashMap parameters = new LinkedHashMap(); + + parameters.put(USERS, users); + parameters.put(GROUPS, groups); + parameters.put(DEFAULT_ROLLOUT_PERCENTAGE, defautPercentage); + + variant.setAssignmentParameters(parameters); + + return variant; + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/FeatureManagerTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/FeatureManagerTest.java new file mode 100644 index 0000000000000..5bbb193402c25 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/FeatureManagerTest.java @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.concurrent.ExecutionException; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; + +import com.azure.spring.cloud.feature.manager.implementation.FeatureManagementConfigProperties; +import com.azure.spring.cloud.feature.manager.implementation.FeatureManagementProperties; +import com.azure.spring.cloud.feature.manager.implementation.models.Feature; +import com.azure.spring.cloud.feature.manager.models.FeatureDefinition; +import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.manager.models.FeatureVariant; +import com.azure.spring.cloud.feature.manager.models.IFeatureFilter; +import com.azure.spring.cloud.feature.manager.models.IFeatureVariantAssigner; + +import reactor.core.publisher.Mono; + +/** + * Unit tests for FeatureManager. + */ +@SpringBootTest(classes = { TestConfiguration.class, SpringBootTest.class }) +public class FeatureManagerTest { + + private FeatureManager featureManager; + + @Mock + private ApplicationContext context; + + @Mock + private FeatureManagementConfigProperties properties; + + @Mock + private FeatureManagementProperties featureManagementPropertiesMock; + + @Mock + private MockFilter filterMock; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + when(properties.isFailFast()).thenReturn(true); + + featureManager = new FeatureManager(context, featureManagementPropertiesMock, properties); + } + + @AfterEach + public void cleanup() throws Exception { + MockitoAnnotations.openMocks(this).close(); + } + + @Test + public void isEnabledFeatureNotFound() { + assertFalse(featureManager.isEnabledAsync("Non Existed Feature").block()); + verify(featureManagementPropertiesMock, times(2)).getOnOff(); + verify(featureManagementPropertiesMock, times(2)).getFeatureManagement(); + } + + @Test + public void isEnabledFeatureOff() { + HashMap features = new HashMap<>(); + features.put("Off", false); + when(featureManagementPropertiesMock.getOnOff()).thenReturn(features); + + assertFalse(featureManager.isEnabledAsync("Off").block()); + verify(featureManagementPropertiesMock, times(2)).getOnOff(); + verify(featureManagementPropertiesMock, times(1)).getFeatureManagement(); + } + + @Test + public void isEnabledOnBoolean() throws InterruptedException, ExecutionException, FilterNotFoundException { + HashMap features = new HashMap<>(); + features.put("On", true); + when(featureManagementPropertiesMock.getOnOff()).thenReturn(features); + + assertTrue(featureManager.isEnabledAsync("On").block()); + verify(featureManagementPropertiesMock, times(2)).getOnOff(); + verify(featureManagementPropertiesMock, times(1)).getFeatureManagement(); + } + + @Test + public void isEnabledFeatureHasNoFilters() { + HashMap features = new HashMap<>(); + Feature noFilters = new Feature(); + noFilters.setKey("NoFilters"); + noFilters.setEnabledFor(new HashMap()); + features.put("NoFilters", noFilters); + when(featureManagementPropertiesMock.getFeatureManagement()).thenReturn(features); + + assertFalse(featureManager.isEnabledAsync("NoFilters").block()); + } + + @Test + public void isEnabledON() throws InterruptedException, ExecutionException, FilterNotFoundException { + HashMap features = new HashMap<>(); + Feature onFeature = new Feature(); + onFeature.setKey("On"); + HashMap filters = new HashMap(); + FeatureFilterEvaluationContext alwaysOn = new FeatureFilterEvaluationContext(); + alwaysOn.setName("AlwaysOn"); + filters.put(0, alwaysOn); + onFeature.setEnabledFor(filters); + features.put("On", onFeature); + when(featureManagementPropertiesMock.getFeatureManagement()).thenReturn(features); + + when(context.getBean(Mockito.matches("AlwaysOn"))).thenReturn(new AlwaysOnFilter()); + + assertTrue(featureManager.isEnabledAsync("On").block()); + } + + @Test + public void noFilter() throws FilterNotFoundException { + HashMap features = new HashMap<>(); + Feature onFeature = new Feature(); + onFeature.setKey("Off"); + HashMap filters = new HashMap(); + FeatureFilterEvaluationContext alwaysOn = new FeatureFilterEvaluationContext(); + alwaysOn.setName("AlwaysOff"); + filters.put(0, alwaysOn); + onFeature.setEnabledFor(filters); + features.put("Off", onFeature); + when(featureManagementPropertiesMock.getFeatureManagement()).thenReturn(features); + + when(context.getBean(Mockito.matches("AlwaysOff"))).thenThrow(new NoSuchBeanDefinitionException("")); + + FilterNotFoundException e = assertThrows(FilterNotFoundException.class, + () -> featureManager.isEnabledAsync("Off").block()); + assertThat(e).hasMessage("Fail fast is set and a Filter was unable to be found: AlwaysOff"); + } + + class MockFilter implements IFeatureFilter, IFeatureVariantAssigner { + + @Override + public Mono assignVariantAsync(FeatureDefinition featureDefinition) { + return null; + } + + @Override + public boolean evaluate(FeatureFilterEvaluationContext context) { + return false; + } + + } + + class AlwaysOnFilter implements IFeatureFilter { + + @Override + public boolean evaluate(FeatureFilterEvaluationContext context) { + return true; + } + + } + +} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/SpringBootTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/SpringBootTest.java similarity index 57% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/SpringBootTest.java rename to sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/SpringBootTest.java index a18faad3e73c8..76468c73beb42 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/SpringBootTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/SpringBootTest.java @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. package com.azure.spring.cloud.feature.manager; import java.lang.annotation.Documented; @@ -11,13 +13,12 @@ import org.springframework.boot.test.context.SpringBootTestContextBootstrapper; import org.springframework.test.context.BootstrapWith; -@Target(value=ElementType.TYPE) -@Retention(value=RetentionPolicy.RUNTIME) +@Target(value = ElementType.TYPE) +@Retention(value = RetentionPolicy.RUNTIME) @Documented @Inherited -@BootstrapWith(value=SpringBootTestContextBootstrapper.class) -@ExtendWith(value=org.springframework.test.context.junit.jupiter.SpringExtension.class) -public @interface SpringBootTest -{ - -} \ No newline at end of file +@BootstrapWith(value = SpringBootTestContextBootstrapper.class) +@ExtendWith(value = org.springframework.test.context.junit.jupiter.SpringExtension.class) +public @interface SpringBootTest { + +} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/TestConfiguration.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/TestConfiguration.java similarity index 69% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/TestConfiguration.java rename to sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/TestConfiguration.java index 158b6a5e9b038..1fab559adf66c 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/TestConfiguration.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/TestConfiguration.java @@ -1,9 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. package com.azure.spring.cloud.feature.manager; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import com.azure.spring.cloud.feature.manager.implementation.FeatureManagementConfigProperties; + @Configuration @ConfigurationProperties public class TestConfiguration { diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilterTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilterTest.java similarity index 96% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilterTest.java rename to sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilterTest.java index 793e6db1db132..f2539cce918f2 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilterTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilterTest.java @@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test; -import com.azure.spring.cloud.feature.manager.entities.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; public class PercentageFilterTest { diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingEvaluatorTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingEvaluatorTest.java new file mode 100644 index 0000000000000..256a6b6b04e2a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingEvaluatorTest.java @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager.feature.filters; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.azure.spring.cloud.feature.manager.TargetingException; +import com.azure.spring.cloud.feature.manager.implementation.targeting.GroupRollout; +import com.azure.spring.cloud.feature.manager.models.FeatureDefinition; +import com.azure.spring.cloud.feature.manager.models.FeatureVariant; +import com.azure.spring.cloud.feature.manager.targeting.ITargetingContextAccessor; +import com.azure.spring.cloud.feature.manager.targeting.TargetingContext; + +import reactor.core.publisher.Mono; + +public class TargetingEvaluatorTest { + + /** + * users field in the filter + */ + protected static final String USERS = "users"; + + /** + * groups field in the filter + */ + protected static final String GROUPS = "groups"; + + /** + * Audience in the filter + */ + protected static final String AUDIENCE = "Audience"; + + @Mock + private FeatureDefinition featureDefinitionMock; + + private TargetingEvaluator targetingEvaluator; + + private LinkedHashMap assignmentParameters; + + private LinkedHashMap assignedUsers; + + private List users; + + private List groups; + + private List variants; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + + targetingEvaluator = new TargetingEvaluator(new TestTargetingContextAccessor()); + + assignmentParameters = new LinkedHashMap<>(); + assignedUsers = new LinkedHashMap<>(); + users = new ArrayList<>(); + groups = new ArrayList<>(); + variants = new ArrayList<>(); + + when(featureDefinitionMock.getVariants()).thenReturn(variants); + + when(featureDefinitionMock.getName()).thenReturn("TestFeatureDefinition"); + } + + @Test + public void evalulateByUser() { + users.add("Doe"); + + assignedUsers.put(USERS, users); + + assignmentParameters.put(AUDIENCE, assignedUsers); + + variants.add(createFeatureVariant(true)); + + FeatureVariant returnedVariant = targetingEvaluator.assignVariantAsync(featureDefinitionMock).block(); + assertNotNull(returnedVariant); + assertEquals(variants.get(0), returnedVariant); + + } + + @Test + public void evalulateByGroup() { + groups = new ArrayList<>(); + + GroupRollout gr = new GroupRollout(); + + gr.setName("G1"); + gr.setRolloutPercentage(100); + + groups.add(gr); + + assignedUsers.put(GROUPS, groups); + + assignmentParameters.put(AUDIENCE, assignedUsers); + + variants.add(createFeatureVariant(true)); + + FeatureVariant returnedVariant = targetingEvaluator.assignVariantAsync(featureDefinitionMock).block(); + assertNotNull(returnedVariant); + assertEquals(variants.get(0), returnedVariant); + + } + + @Test + public void evalulateByDefaultPercentage() { + assignmentParameters.put("defaultRolloutPercentage", 100); + + variants.add(createFeatureVariant(true)); + + FeatureVariant returnedVariant = targetingEvaluator.assignVariantAsync(featureDefinitionMock).block(); + assertNotNull(returnedVariant); + assertEquals(variants.get(0), returnedVariant); + } + + @Test + public void evalulateByDefault() { + variants.add(createFeatureVariant(true)); + + FeatureVariant returnedVariant = targetingEvaluator.assignVariantAsync(featureDefinitionMock).block(); + assertNotNull(returnedVariant); + assertEquals(variants.get(0), returnedVariant); + } + + @Test + public void noContextAccessor() { + assertEquals(Mono.justOrEmpty(null), new TargetingEvaluator(null).assignVariantAsync(featureDefinitionMock)); + } + + @Test + public void noContext() { + assertEquals(Mono.justOrEmpty(null), new TargetingEvaluator(new InvlaidTargetingContextAccessor()) + .assignVariantAsync(featureDefinitionMock)); + } + + @Test + public void groupOutOfRange() { + groups = new ArrayList<>(); + + GroupRollout gr = new GroupRollout(); + + gr.setName("G1"); + gr.setRolloutPercentage(50); + + groups.add(gr); + + gr.setName("G2"); + gr.setRolloutPercentage(51); + + groups.add(gr); + + assignedUsers.put(GROUPS, groups); + + assignmentParameters.put(AUDIENCE, assignedUsers); + + variants.add(createFeatureVariant(true)); + + assertThrows(TargetingException.class, + () -> targetingEvaluator.assignVariantAsync(featureDefinitionMock).block()); + } + + @Test + public void defaultPercentageOutOfRange() { + assignmentParameters.put("defaultRolloutPercentage", 101); + + variants.add(createFeatureVariant(true)); + + assertThrows(TargetingException.class, + () -> targetingEvaluator.assignVariantAsync(featureDefinitionMock).block()); + + } + + private FeatureVariant createFeatureVariant(Boolean isDefault) { + FeatureVariant featureVariant = new FeatureVariant(); + + featureVariant.setAssignmentParameters(assignmentParameters); + + featureVariant.setDefault(isDefault); + + return featureVariant; + } + + private class TestTargetingContextAccessor implements ITargetingContextAccessor { + + @Override + public Mono getContextAsync() { + TargetingContext context = new TargetingContext(); + context.setUserId("Doe"); + + List groups = new ArrayList<>(); + groups.add("G1"); + + context.setGroups(groups); + return Mono.just(context); + } + + } + + private class InvlaidTargetingContextAccessor implements ITargetingContextAccessor { + + @Override + public Mono getContextAsync() { + return Mono.justOrEmpty(null); + } + + } + +} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingFilterTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingFilterTest.java similarity index 99% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingFilterTest.java rename to sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingFilterTest.java index 72d8bebbc7023..9391695746c35 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingFilterTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TargetingFilterTest.java @@ -17,7 +17,7 @@ import com.azure.spring.cloud.feature.manager.TargetingException; import com.azure.spring.cloud.feature.manager.TestConfiguration; -import com.azure.spring.cloud.feature.manager.entities.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; import com.azure.spring.cloud.feature.manager.targeting.ITargetingContextAccessor; import com.azure.spring.cloud.feature.manager.targeting.TargetingContext; import com.azure.spring.cloud.feature.manager.targeting.TargetingEvaluationOptions; diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilterTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilterTest.java similarity index 97% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilterTest.java rename to sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilterTest.java index 18b7d852d79a4..371020fd5448b 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilterTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilterTest.java @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test; -import com.azure.spring.cloud.feature.manager.entities.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; public class TimeWindowFilterTest { diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementPropertiesTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementPropertiesTest.java new file mode 100644 index 0000000000000..395ab52118cba --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementPropertiesTest.java @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager.implementation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import org.junit.jupiter.api.Test; + +import com.azure.spring.cloud.feature.manager.FilterNotFoundException; +import com.azure.spring.cloud.feature.manager.implementation.models.DynamicFeature; +import com.azure.spring.cloud.feature.manager.implementation.models.Feature; +import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; +import com.azure.spring.cloud.feature.manager.models.FeatureVariant; + +public class FeatureManagementPropertiesTest { + + private static final String FEATURE_KEY = "TestFeature"; + + private static final String FILTER_NAME = "Filter1"; + + private static final String PARAM_1_NAME = "param1"; + + private static final String PARAM_1_VALUE = "testParam"; + + /** + * Tests the conversion that takes place when data comes from EnumerablePropertySource. + */ + @Test + public void loadFeatureManagerWithLinkedHashSet() { + Feature f = new Feature(); + f.setKey(FEATURE_KEY); + + LinkedHashMap testMap = new LinkedHashMap(); + LinkedHashMap testFeature = new LinkedHashMap(); + LinkedHashMap enabledFor = new LinkedHashMap(); + LinkedHashMap ffec = new LinkedHashMap(); + LinkedHashMap parameters = new LinkedHashMap(); + ffec.put("name", FILTER_NAME); + parameters.put(PARAM_1_NAME, PARAM_1_VALUE); + ffec.put("parameters", parameters); + enabledFor.put("0", ffec); + testFeature.put("enabled-for", enabledFor); + testMap.put(f.getKey(), testFeature); + + FeatureManagementProperties properties = new FeatureManagementProperties(); + properties.putAll(testMap); + assertNotNull(properties.getFeatureManagement()); + assertEquals(1, properties.getFeatureManagement().size()); + assertNotNull(properties.getFeatureManagement().get(FEATURE_KEY)); + Feature feature = properties.getFeatureManagement().get(FEATURE_KEY); + assertEquals(FEATURE_KEY, feature.getKey()); + assertEquals(1, feature.getEnabledFor().size()); + FeatureFilterEvaluationContext zeroth = feature.getEnabledFor().get(0); + assertEquals(FILTER_NAME, zeroth.getName()); + assertEquals(1, zeroth.getParameters().size()); + assertEquals(PARAM_1_VALUE, zeroth.getParameters().get(PARAM_1_NAME)); + } + + @Test + public void isEnabledPeriodSplit() throws InterruptedException, ExecutionException, FilterNotFoundException { + LinkedHashMap features = new LinkedHashMap<>(); + LinkedHashMap featuresOn = new LinkedHashMap<>(); + + featuresOn.put("A", true); + features.put("Beta", featuresOn); + + FeatureManagementProperties properties = new FeatureManagementProperties(); + properties.putAll(features); + + assertTrue(properties.getOnOff().get("Beta.A")); + } + + @Test + public void isEnabledInvalid() throws InterruptedException, ExecutionException, FilterNotFoundException { + LinkedHashMap features = new LinkedHashMap(); + LinkedHashMap featuresOn = new LinkedHashMap(); + + featuresOn.put("A", 5); + features.put("Beta", featuresOn); + + FeatureManagementProperties properties = new FeatureManagementProperties(); + properties.putAll(features); + + assertNull(properties.getOnOff().getOrDefault("Beta.A", null)); + assertEquals(0, properties.size()); + } + + @Test + public void bootstrapConfiguration() { + HashMap features = new HashMap(); + features.put("FeatureU", false); + Feature featureV = new Feature(); + HashMap filterMapper = new HashMap(); + + FeatureFilterEvaluationContext enabledFor = new FeatureFilterEvaluationContext(); + enabledFor.setName("Random"); + + LinkedHashMap parameters = new LinkedHashMap(); + parameters.put("chance", "50"); + + enabledFor.setParameters(parameters); + filterMapper.put(0, enabledFor); + featureV.setEnabledFor(filterMapper); + features.put("FeatureV", featureV); + + FeatureManagementProperties properties = new FeatureManagementProperties(); + properties.putAll(features); + + assertNotNull(properties.getOnOff()); + assertNotNull(properties.getFeatureManagement()); + + assertEquals(properties.getOnOff().get("FeatureU"), false); + Feature feature = properties.getFeatureManagement().get("FeatureV"); + assertEquals(feature.getEnabledFor().size(), 1); + FeatureFilterEvaluationContext ffec = feature.getEnabledFor().get(0); + assertEquals(ffec.getName(), "Random"); + assertEquals(ffec.getParameters().size(), 1); + assertEquals(ffec.getParameters().get("chance"), "50"); + } + + @Test + public void featureVariantLoadTest() { + HashMap features = new HashMap(); + + DynamicFeature df = new DynamicFeature(); + df.setAssigner("Microsoft.Targeting"); + + FeatureVariant fv = new FeatureVariant(); + fv.setName("TestVariant"); + fv.setDefault(true); + fv.setConfigurationReference("config.reference"); + + LinkedHashMap assignmentParameters = new LinkedHashMap<>(); + + assignmentParameters.put("User", "Doe"); + + fv.setAssignmentParameters(assignmentParameters); + + Map variants = new HashMap<>(); + + variants.put("TestVariant", fv); + + df.setVariants(variants); + + features.put("TestDynamicFeature", df); + + FeatureManagementProperties properties = new FeatureManagementProperties(); + properties.putAll(features); + + assertNotNull(properties.getDynamicFeatures().get("TestDynamicFeature")); + } +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/BasicObject.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/BasicObject.java new file mode 100644 index 0000000000000..fa0ff2d6d4ea5 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/BasicObject.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager.testobjects; + +public class BasicObject { + + private String testValue; + + /** + * @return the testValue + */ + public String getTestValue() { + return testValue; + } + + /** + * @param testValue the testValue to set + */ + public void setTestValue(String testValue) { + this.testValue = testValue; + } + +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/DiscountBanner.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/DiscountBanner.java new file mode 100644 index 0000000000000..378de1a6c044e --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/DiscountBanner.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager.testobjects; + +public class DiscountBanner { + + private Integer size; + + private String color; + + public Integer getSize() { + return size; + } + + public DiscountBanner setSize(Integer size) { + this.size = size; + return this; + } + + public String getColor() { + return color; + } + + public DiscountBanner setColor(String color) { + this.color = color; + return this; + } + + @Override + public String toString() { + return "DiscountBannder: Size " + size + " Color " + color; + } +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/MockableProperties.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/MockableProperties.java new file mode 100644 index 0000000000000..cd5d2cedc01a5 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/MockableProperties.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.manager.testobjects; + +import java.util.Map; + +import com.azure.spring.cloud.feature.manager.IDynamicFeatureProperties; + +public class MockableProperties implements IDynamicFeatureProperties { + + private Map discountBanner; + + /** + * @return the discountBanner + */ + public Map getDiscountBanner() { + return discountBanner; + } + + /** + * @param discountBanner the discountBanner to set + */ + public void setDiscountBanner(Map discountBanner) { + this.discountBanner = discountBanner; + } + +} diff --git a/sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/resources/application.yaml b/sdk/spring/spring-cloud-azure-feature-management/src/test/resources/application.yaml similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/src/test/resources/application.yaml rename to sdk/spring/spring-cloud-azure-feature-management/src/test/resources/application.yaml diff --git a/sdk/appconfiguration/azure-spring-cloud-starter-appconfiguration-config/CHANGELOG.md b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/CHANGELOG.md similarity index 95% rename from sdk/appconfiguration/azure-spring-cloud-starter-appconfiguration-config/CHANGELOG.md rename to sdk/spring/spring-cloud-azure-starter-appconfiguration-config/CHANGELOG.md index 373bd32985ed0..c153882ba5e38 100644 --- a/sdk/appconfiguration/azure-spring-cloud-starter-appconfiguration-config/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 2.12.0-beta.1 (Unreleased) +## 4.0.0-beta.1 (Unreleased) ### Features Added @@ -10,12 +10,6 @@ ### Other Changes -## 2.11.0 (2023-01-18) -Upgrade Spring Boot dependencies version to 2.7.7 and Spring Cloud dependencies version to 2021.0.5 - -### Bugs Fixed -- Updating to SDK 1.4.1 to fix sync token issue. - ## 2.10.0 (2022-11-24) - This release is compatible with Spring Boot 2.5.0-2.5.14, 2.6.0-2.6.13, 2.7.0-2.7.5. (Note: 2.5.x (x>14), 2.6.y (y>13) and 2.7.z (z>5) should be supported, but they aren't tested with this release.) - This release is compatible with Spring Cloud 2020.0.3-2020.0.6, 2021.0.0-2021.0.5. (Note: 2020.0.x (x>6) and 2021.0.y (y>5) should be supported, but they aren't tested with this release.) diff --git a/sdk/appconfiguration/azure-spring-cloud-starter-appconfiguration-config/README.md b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md similarity index 99% rename from sdk/appconfiguration/azure-spring-cloud-starter-appconfiguration-config/README.md rename to sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md index 149b0105f0740..299acc055190c 100644 --- a/sdk/appconfiguration/azure-spring-cloud-starter-appconfiguration-config/README.md +++ b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md @@ -21,7 +21,7 @@ There are two libraries that can be used azure-spring-cloud-appconfiguration-con com.azure.spring azure-spring-cloud-appconfiguration-config - 2.11.0 + 4.0.0 ``` [//]: # ({x-version-update-end}) @@ -33,7 +33,7 @@ or com.azure.spring azure-spring-cloud-appconfiguration-config-web - 2.11.0 + 4.0.0 ``` [//]: # ({x-version-update-end}) diff --git a/sdk/appconfiguration/azure-spring-cloud-starter-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml similarity index 91% rename from sdk/appconfiguration/azure-spring-cloud-starter-appconfiguration-config/pom.xml rename to sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml index 8b77bcc6a2a5c..234ae06c7e42c 100644 --- a/sdk/appconfiguration/azure-spring-cloud-starter-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml @@ -11,19 +11,19 @@ com.azure.spring azure-spring-cloud-starter-appconfiguration-config - 2.12.0-beta.1 + 4.0.0-beta.1 Azure Spring Cloud Starter App Configuration Config com.azure.spring azure-spring-cloud-appconfiguration-config-web - 2.12.0-beta.1 + 4.0.0-beta.1 com.azure.spring azure-spring-cloud-feature-management-web - 2.11.0-beta.1 + 4.0.0-beta.1 diff --git a/sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/CHANGELOG.md b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/CHANGELOG.md similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/CHANGELOG.md rename to sdk/spring/spring-cloud-azure-test-appconfiguration-config/CHANGELOG.md diff --git a/sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/README.md b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/README.md similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/README.md rename to sdk/spring/spring-cloud-azure-test-appconfiguration-config/README.md diff --git a/sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml similarity index 91% rename from sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/pom.xml rename to sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml index c3f43ded4c3e8..0790e0658cd76 100644 --- a/sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml @@ -8,7 +8,7 @@ org.springframework.boot spring-boot-starter-parent - 2.7.8 + 2.7.7 @@ -24,7 +24,7 @@ com.azure.spring azure-spring-cloud-starter-appconfiguration-config - 2.12.0-beta.1 + 4.0.0-beta.1 com.microsoft.azure @@ -38,7 +38,7 @@ org.springframework.boot spring-boot-starter-test - 2.7.8 + 2.7.7 test diff --git a/sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java rename to sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java diff --git a/sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java rename to sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java diff --git a/sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MessageProperties.java b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MessageProperties.java similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MessageProperties.java rename to sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MessageProperties.java diff --git a/sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MyCredentials.java b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MyCredentials.java similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MyCredentials.java rename to sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MyCredentials.java diff --git a/sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/src/test/resources/META-INF/spring.factories b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/resources/META-INF/spring.factories similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/src/test/resources/META-INF/spring.factories rename to sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/resources/META-INF/spring.factories diff --git a/sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/test-resources.json b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/test-resources.json similarity index 100% rename from sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/test-resources.json rename to sdk/spring/spring-cloud-azure-test-appconfiguration-config/test-resources.json From cebd01c8c5ca2a2fee2409849fa372e3f4a5cab8 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 6 Feb 2023 12:43:11 -0800 Subject: [PATCH 02/38] Fixing name in external places and updating RevApi --- eng/.docsettings.yml | 6 +- .../src/main/resources/revapi/revapi.json | 85 ++++++++----------- eng/versioning/version_client.txt | 10 +-- sdk/spring/pom.xml | 4 +- .../pom.xml | 5 -- .../README.md | 4 +- .../pom.xml | 54 +++++++----- .../pom.xml | 14 +-- 8 files changed, 89 insertions(+), 93 deletions(-) diff --git a/eng/.docsettings.yml b/eng/.docsettings.yml index 13d324e0dd67f..12ec946c43ae2 100644 --- a/eng/.docsettings.yml +++ b/eng/.docsettings.yml @@ -147,9 +147,9 @@ known_content_issues: - ['sdk/storage/README.md', '#3113'] - ['sdk/tools/azure-sdk-archetype/README.md', '#3113'] - ['sdk/tools/azure-sdk-build-tool/README.md', '#3113'] - - ['sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/README.md', '#3113'] - - ['sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/README.md', '#3113'] - - ['sdk/appconfiguration/azure-spring-cloud-feature-management-web/README.md', '#3113'] + - ['sdk/appconfiguration/spring-cloud-azure-appconfiguration-config-web/README.md', '#3113'] + - ['sdk/appconfiguration/spring-cloud-azure-appconfiguration-config/README.md', '#3113'] + - ['sdk/appconfiguration/spring-cloud-azure-feature-management-web/README.md', '#3113'] package_indexing_exclusion_list: - azure-loganalytics-sample diff --git a/eng/code-quality-reports/src/main/resources/revapi/revapi.json b/eng/code-quality-reports/src/main/resources/revapi/revapi.json index d9b70a86659fa..305256c209e47 100644 --- a/eng/code-quality-reports/src/main/resources/revapi/revapi.json +++ b/eng/code-quality-reports/src/main/resources/revapi/revapi.json @@ -76,11 +76,6 @@ "matcher": "java-package", "match": "/com\\.azure\\.data\\.cosmos(\\..*)?/" }, - "class com\\.azure\\.spring\\.cloud\\.config\\.State", - "class com\\.azure\\.spring\\.cloud\\.config\\.(AppConfigurationRefresh|AppConfigurationBootstrapConfiguration)", - "class com\\.azure\\.spring\\.cloud\\.config\\.properties\\.AppConfigurationProviderProperties", - "class com\\.azure\\.spring\\.cloud\\.config\\.web\\.pushrefresh\\.AppConfigurationRefreshEvent", - "class com\\.azure\\.spring\\.cloud\\.config\\.web\\.AppConfigurationEndpoint", { "matcher": "java-package", "match": "/org\\.apache\\.(avro|commons|qpid)(\\..*)?/" @@ -303,40 +298,6 @@ "old": "method void com.azure.spring.cloud.autoconfigure.jms.properties.AzureServiceBusJmsProperties::setUsername(java.lang.String)", "justification": "Remove some meaningless jms properties" }, - { - "regex": true, - "code": "java.method.numberOfParametersChanged", - "old": "method void com\\.azure\\.spring\\.cloud\\.config\\.stores\\..*", - "justification": "Not a public api" - }, - { - "code": "java.method.removed", - "old": "method com.azure.spring.cloud.config.resource.ConnectionPool com.azure.spring.cloud.config.AppConfigurationBootstrapConfiguration::initConnectionString(com.azure.spring.cloud.config.properties.AppConfigurationProperties)", - "justification": "Not a public api" - }, - { - "regex": true, - "code": "java.class.removed", - "old": "class com\\.azure\\.spring\\.cloud\\.config\\.resource\\.(Connection|ConnectionPool)", - "justification": "Not a public api" - }, - { - "regex": true, - "code": "java.class.removed", - "old": "class com\\.azure\\.spring\\.cloud\\.config\\.(AppConfigurationPropertySource|AppConfigurationPropertySourceLocator)", - "justification": "Not a public api" - }, - { - "code": "java.class.removed", - "old": "class com.azure.spring.cloud.config.stores.ClientStore", - "justification": "Not a public api" - }, - { - "ignore": true, - "code": "java.class.externalClassExposedInAPI", - "new": "interface com.azure.spring.cloud.config.AppConfigurationRefresh", - "justification": "Thi isn't an external class" - }, { "regex": true, "code": "java\\.class\\.externalClassExposedInAPI", @@ -381,17 +342,6 @@ "new": "method com.azure.search.documents.indexes.models.(CognitiveServicesAccountKey|ConditionalSkill|CustomEntityLookupSkill|DefaultCognitiveServicesAccount|DistanceScoringFunction|DocumentExtractionSkill|EntityRecognitionSkill|FreshnessScoringFunction|ImageAnalysisSkill|KeyPhraseExtractionSkill|LanguageDetectionSkill|MagnitudeScoringFunction|MergeSkill|OcrSkill|SentimentSkill|ShaperSkill|SplitSkill|TagScoringFunction|TextTranslationSkill|WebApiSkill) .*", "justification": "Proper support for fluent setters in subtypes." }, - { - "code": "java.method.numberOfParametersChanged", - "old": "method void com.azure.spring.cloud.config.pipline.policies.BaseAppConfigurationPolicy::(java.lang.Boolean, java.lang.Boolean)", - "new": "method void com.azure.spring.cloud.config.pipline.policies.BaseAppConfigurationPolicy::(java.lang.Boolean, java.lang.Boolean, java.lang.Integer)", - "justification": "Not a public api" - }, - { - "code": "java.field.removedWithConstant", - "old": "field com.azure.spring.cloud.config.properties.AppConfigurationStoreSelects.LABEL_SEPARATOR", - "justification": "Not a public api" - }, { "code": "java.method.numberOfParametersChanged", "new": "method void com.azure.spring.cloud.autoconfigure.aad.AadAuthenticationFilterAutoConfiguration::(com.azure.spring.cloud.autoconfigure.aad.properties.AadAuthenticationProperties, org.springframework.boot.web.client.RestTemplateBuilder)", @@ -791,6 +741,41 @@ "old": "field com.azure.messaging.eventgrid.SystemEventNames.SERVICE_BUS_DEADLETTER_MESSAGES_AVAILABLE_WITH_NO_LISTENER", "new": "field com.azure.messaging.eventgrid.SystemEventNames.SERVICE_BUS_DEADLETTER_MESSAGES_AVAILABLE_WITH_NO_LISTENER", "justification": "Previous constant value had a typo and was never functional." + }, + { + "code": "java.class.externalClassExposedInAPI", + "new": "class com.azure.data.appconfiguration.models.SettingSelector", + "justification": "This is an Azure SDK class that is used in the public API." + }, + { + "code": "java.class.externalClassExposedInAPI", + "new": "class com.azure.data.appconfiguration.models.SettingFields", + "justification": "This is an Azure SDK class that is used in the public API." + }, + { + "code": "java.class.externalClassExposedInAPI", + "new": "class com.azure.data.appconfiguration.models.ConfigurationSetting", + "justification": "This is an Azure SDK class that is used in the public API." + }, + { + "code": "java.class.externalClassExposedInAPI", + "new": "enum com.azure.data.appconfiguration.ConfigurationServiceVersion", + "justification": "This is an Azure SDK class that is used in the public API." + }, + { + "code": "java.class.externalClassExposedInAPI", + "new": "class com.azure.data.appconfiguration.ConfigurationClientBuilder", + "justification": "This is an Azure SDK class that is used in the public API." + }, + { + "code": "java.class.externalClassExposedInAPI", + "new": "class com.azure.data.appconfiguration.ConfigurationClient", + "justification": "This is an Azure SDK class that is used in the public API." + }, + { + "code": "java.class.externalClassExposedInAPI", + "new": "class com.azure.data.appconfiguration.ConfigurationAsyncClient", + "justification": "This is an Azure SDK class that is used in the public API." } ] } diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 794e4e3de277d..517cd1833d942 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -177,11 +177,11 @@ com.azure:perf-test-core;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-communication-email;1.0.0-beta.1;1.0.0-beta.2 com.azure:azure-developer-loadtesting;1.0.0-beta.2;1.0.0-beta.3 com.azure:azure-identity-extensions;1.1.0;1.2.0-beta.1 -com.azure.spring:azure-spring-cloud-appconfiguration-config-web;2.11.0;2.12.0-beta.1 -com.azure.spring:azure-spring-cloud-appconfiguration-config;2.11.0;2.12.0-beta.1 -com.azure.spring:azure-spring-cloud-feature-management-web;2.10.0;2.11.0-beta.1 -com.azure.spring:azure-spring-cloud-feature-management;2.10.0;2.11.0-beta.1 -com.azure.spring:azure-spring-cloud-starter-appconfiguration-config;2.11.0;2.12.0-beta.1 +com.azure.spring:spring-cloud-azure-appconfiguration-config-web;2.11.0;4.0.0-beta.1 +com.azure.spring:spring-cloud-azure-appconfiguration-config;2.11.0;4.0.0-beta.1 +com.azure.spring:spring-cloud-azure-feature-management-web;2.10.0;4.0.0-beta.1 +com.azure.spring:spring-cloud-azure-feature-management;2.10.0;4.0.0-beta.1 +com.azure.spring:spring-cloud-azure-starter-appconfiguration-config;2.11.0;4.0.0-beta.1 com.azure.spring:spring-cloud-azure-dependencies;4.5.0;4.6.0-beta.1 com.azure.spring:spring-messaging-azure;4.5.0;4.6.0-beta.1 com.azure.spring:spring-messaging-azure-eventhubs;4.5.0;4.6.0-beta.1 diff --git a/sdk/spring/pom.xml b/sdk/spring/pom.xml index a480a102467e4..ab2b5e96d0589 100644 --- a/sdk/spring/pom.xml +++ b/sdk/spring/pom.xml @@ -62,7 +62,7 @@ spring-cloud-azure-integration-tests spring-cloud-azure-test-appconfiguration-config spring-cloud-azure-appconfiguration-config - spring-cloud-appconfiguration-config-web + spring-cloud-azure-appconfiguration-config-web spring-cloud-azure-feature-management spring-cloud-azure-feature-management-web spring-cloud-azure-starter-appconfiguration-config @@ -120,7 +120,7 @@ spring-cloud-azure-starter-redis spring-cloud-azure-test-appconfiguration-config spring-cloud-azure-appconfiguration-config - spring-cloud-appconfiguration-config-web + spring-cloud-azure-appconfiguration-config-web spring-cloud-azure-feature-management spring-cloud-azure-feature-management-web spring-cloud-azure-starter-appconfiguration-config diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index 84a7ddcf26aa1..dfde10e33a458 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -98,11 +98,6 @@ spring-cloud-azure-service 4.4.1 - - com.azure.spring - spring-cloud-azure-service - 4.4.1 - com.azure.spring spring-cloud-azure-actuator-autoconfigure diff --git a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md index 299acc055190c..9b069ff2ac166 100644 --- a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md +++ b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md @@ -21,7 +21,7 @@ There are two libraries that can be used azure-spring-cloud-appconfiguration-con com.azure.spring azure-spring-cloud-appconfiguration-config - 4.0.0 + 2.10.0 ``` [//]: # ({x-version-update-end}) @@ -33,7 +33,7 @@ or com.azure.spring azure-spring-cloud-appconfiguration-config-web - 4.0.0 + 2.10.0 ``` [//]: # ({x-version-update-end}) diff --git a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml index 234ae06c7e42c..8bd1a72566e24 100644 --- a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml @@ -1,6 +1,7 @@ - + com.azure azure-client-sdk-parent @@ -10,26 +11,30 @@ 4.0.0 com.azure.spring - azure-spring-cloud-starter-appconfiguration-config - 4.0.0-beta.1 + spring-cloud-azure-starter-appconfiguration-config + 4.0.0-beta.1 Azure Spring Cloud Starter App Configuration Config com.azure.spring - azure-spring-cloud-appconfiguration-config-web - 4.0.0-beta.1 + spring-cloud-azure-appconfiguration-config-web + 4.0.0-beta.1 com.azure.spring - azure-spring-cloud-feature-management-web - 4.0.0-beta.1 + spring-cloud-azure-feature-management-web + 4.0.0-beta.1 - + 3.1.2 empty-javadoc-jar-with-readme @@ -49,7 +55,8 @@ jar - javadoc + + javadoc ${project.basedir}/javadocTemp @@ -60,7 +67,8 @@ jar - sources + + sources ${project.basedir}/sourceTemp @@ -69,27 +77,33 @@ org.apache.maven.plugins maven-antrun-plugin - 1.8 + 1.8 copy-readme-to-javadocTemp-and-sourceTemp prepare-package - Deleting existing ${project.basedir}/javadocTemp and + Deleting + existing ${project.basedir}/javadocTemp and ${project.basedir}/sourceTemp - - + + - Copying ${project.basedir}/../README.md to + Copying + ${project.basedir}/../README.md to ${project.basedir}/javadocTemp/README.md - - Copying ${project.basedir}/../README.md to + + Copying + ${project.basedir}/../README.md to ${project.basedir}/sourceTemp/README.md - + diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml index 0790e0658cd76..d55776fe9521a 100644 --- a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml @@ -13,8 +13,8 @@ com.azure.spring - azure-spring-cloud-test-appconfiguration-config - 1.0.0 + spring-cloud-azure-test-appconfiguration-config + 1.0.0 true @@ -23,8 +23,8 @@ com.azure.spring - azure-spring-cloud-starter-appconfiguration-config - 4.0.0-beta.1 + spring-cloud-azure-starter-appconfiguration-config + 4.0.0-beta.1 com.microsoft.azure @@ -56,7 +56,8 @@ integration-test - ${skipSpringITs} + + ${skipSpringITs} @@ -66,7 +67,8 @@ maven-surefire-plugin 2.22.0 - ${skipSpringITs} + + ${skipSpringITs} From a8751c975e773f4080895e5cf2c16f7686af862c Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 6 Feb 2023 12:51:40 -0800 Subject: [PATCH 03/38] Removing Dynamic Features as it might not be part of the 4.0 --- .../manager/DynamicFeatureException.java | 31 -- .../manager/DynamicFeatureManager.java | 170 ----------- .../FeatureManagementConfiguration.java | 17 -- .../manager/IDynamicFeatureProperties.java | 10 - .../FeatureManagementProperties.java | 20 +- .../implementation/models/DynamicFeature.java | 48 ---- .../manager/DynamicFeatureManagerTest.java | 272 ------------------ .../FeatureManagementPropertiesTest.java | 160 ----------- .../testobjects/MockableProperties.java | 27 -- 9 files changed, 1 insertion(+), 754 deletions(-) delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureException.java delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManager.java delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/IDynamicFeatureProperties.java delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/DynamicFeature.java delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerTest.java delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementPropertiesTest.java delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/MockableProperties.java diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureException.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureException.java deleted file mode 100644 index c32d50f561262..0000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureException.java +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager; - -/** - * Error thrown when an issue is found while generating a Feature Variant. - */ -public final class DynamicFeatureException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - /** - * Creates a new instance of the DynamicFeatureException - * - * @param message the error message. - * @param cause original issue caught - */ - DynamicFeatureException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Creates a new instance of the DynamicFeatureException - * - * @param message the error message. - */ - DynamicFeatureException(String message) { - super(message); - } - -} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManager.java deleted file mode 100644 index c2f8a220dc07a..0000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManager.java +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Map; -import java.util.Optional; - -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.context.ApplicationContext; -import org.springframework.util.StringUtils; - -import com.azure.spring.cloud.feature.manager.implementation.FeatureManagementProperties; -import com.azure.spring.cloud.feature.manager.implementation.models.DynamicFeature; -import com.azure.spring.cloud.feature.manager.models.FeatureDefinition; -import com.azure.spring.cloud.feature.manager.models.FeatureVariant; -import com.azure.spring.cloud.feature.manager.models.IFeatureVariantAssigner; - -import reactor.core.publisher.Mono; - -/** - * Holds information on Feature Management properties and can check if a given feature is enabled. - */ -public class DynamicFeatureManager { - - private transient ApplicationContext context; - - /** - * ConfigurationProperties for accessing the different types of feature variants. - */ - private final ObjectProvider propertiesProvider; - - private final FeatureManagementProperties featureManagementConfigurations; - - /** - * Creates Dynamic Feature Manager - * - * @param context ApplicationContext - * @param propertiesProvider Object Provider for accessing client IDynamicFeatureProperties - * @param featureManagementConfigurations Configuration Properties for Feature Flags - */ - DynamicFeatureManager(ApplicationContext context, - ObjectProvider propertiesProvider, - FeatureManagementProperties featureManagementConfigurations) { - this.context = context; - this.propertiesProvider = propertiesProvider; - this.featureManagementConfigurations = featureManagementConfigurations; - } - - /** - * Returns a feature variant of the type given. - * - * @param Type of the feature that will be returned. - * @param variantName name of the feature being checked. - * @param returnClass Type of the feature being checked. - * @return variant of the provided type - */ - public Mono getVariantAsync(String variantName, Class returnClass) { - return generateVariant(variantName, returnClass); - } - - @SuppressWarnings("unchecked") - private Mono generateVariant(String featureName, Class type) { - - if (!StringUtils.hasText(featureName)) { - throw new IllegalArgumentException("Feature Variant name can not be empty or null."); - } - - if (!featureManagementConfigurations.getDynamicFeatures().containsKey(featureName)) { - throw new FeatureManagementException("The Dynamic Feature " + featureName + " can not be found."); - } - - DynamicFeature dynamicFeature = featureManagementConfigurations.getDynamicFeatures().get(featureName); - - FeatureDefinition featureDefinition = new FeatureDefinition(featureName, dynamicFeature.getAssigner(), - dynamicFeature.getVariants()); - - validateDynamicFeature(featureDefinition, featureName); - - try { - IFeatureVariantAssigner assigner = (IFeatureVariantAssigner) context - .getBean(featureDefinition.getAssigner()); - return (Mono) assigner.assignVariantAsync(featureDefinition).map(this::assignVariant); - } catch (NoSuchBeanDefinitionException e) { - throw new FeatureManagementException("The feature variant assigner " + featureDefinition.getAssigner() - + " specified for feature " + featureName + " was not found."); - } - } - - @SuppressWarnings("unchecked") - private T assignVariant(FeatureVariant variant) { - String reference = variant.getConfigurationReference(); - - String[] parts = reference.split("\\."); - - String methodName = "get" + parts[0]; - Method method = null; - Map variantMap = null; - - Optional variantProperties = propertiesProvider.stream().filter(properties -> { - try { - properties.getClass().getMethod(methodName); - return true; - } catch (NoSuchMethodException | SecurityException e) { - return false; - } - }).findFirst(); - - if (!variantProperties.isPresent()) { - String message = "Failed to load " + methodName + ". No ConfigurationProperties where found containing it." - + ". Make sure it exists and is publicly accessible."; - throw new DynamicFeatureException(message); - } - - // Dynamically Accesses @ConfigurationProperties and finds the matching method. - try { - method = variantProperties.get().getClass().getMethod(methodName); - } catch (NoSuchMethodException | SecurityException e) { - String message = "Failed to load " + methodName + " in " + variantProperties.getClass() - + ". Make sure it exists and is publicly accessible."; - throw new DynamicFeatureException(message, e); - } - // Calls method to get back an Object, this object contains multiple variants - // each has a get method. - try { - variantMap = (Map) method.invoke(variantProperties.get()); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - String message = "Failed invoking " + methodName + " in " + variantProperties.getClass() - + ". Make sure it exists and is publicly accessible."; - throw new DynamicFeatureException(message, e); - } - - return variantMap.get(parts[1]); - } - - private void validateDynamicFeature(FeatureDefinition featureDefinition, String featureName) - throws FeatureManagementException { - if (!StringUtils.hasText(featureDefinition.getAssigner())) { - throw new FeatureManagementException( - "Missing Feature Variant assigner name for the feature " + featureName); - } - - if (featureDefinition.getVariants() == null || featureDefinition.getVariants().size() == 0) { - throw new FeatureManagementException("No Variants are registered for the feature " + featureName); - } - - FeatureVariant defaultVariant = null; - - for (FeatureVariant v : featureDefinition.getVariants()) { - if (v.getDefault()) { - if (defaultVariant != null) { - throw new FeatureManagementException( - "Multiple default variants are registered for the feature " + featureName); - } - defaultVariant = v; - } - - if (!StringUtils.hasText(v.getConfigurationReference())) { - throw new FeatureManagementException("The variant " + v.getName() + " for the feature " + featureName - + " does not have a configuration reference."); - } - } - - if (defaultVariant == null) { - throw new FeatureManagementException("A default variant cannot be found for the feature " + featureName); - } - } -} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java index 023446698d462..e08394c360560 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java @@ -2,7 +2,6 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.manager; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -31,20 +30,4 @@ FeatureManager featureManager(ApplicationContext context, FeatureManagementProperties featureManagementConfigurations, FeatureManagementConfigProperties properties) { return new FeatureManager(context, featureManagementConfigurations, properties); } - - /** - * Creates Dynamic Feature Manager - * - * @param context ApplicationContext - * @param propertiesProvider Object Provider for accessing client IDynamicFeatureProperties - * @param featureManagementConfigurations Configuration Properties for Feature Flags - * @return DynamicFeatureManager - */ - @Bean - DynamicFeatureManager dynamicFeatureManager(ApplicationContext context, - ObjectProvider propertiesProvider, - FeatureManagementProperties featureManagementConfigurations) { - return new DynamicFeatureManager(context, propertiesProvider, featureManagementConfigurations); - } - } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/IDynamicFeatureProperties.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/IDynamicFeatureProperties.java deleted file mode 100644 index 429690b83021d..0000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/IDynamicFeatureProperties.java +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager; - -/** - * Properties interface for Azure App Configuration Dynamic Features - */ -public interface IDynamicFeatureProperties { - -} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementProperties.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementProperties.java index 4e06803f53374..17d8dcf510edd 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementProperties.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementProperties.java @@ -9,9 +9,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.util.StringUtils; -import com.azure.spring.cloud.feature.manager.implementation.models.DynamicFeature; import com.azure.spring.cloud.feature.manager.implementation.models.Feature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -37,12 +35,9 @@ public class FeatureManagementProperties extends HashMap { */ private Map onOff; - private transient Map dynamicFeatures; - public FeatureManagementProperties() { featureManagement = new HashMap<>(); onOff = new HashMap<>(); - dynamicFeatures = new HashMap<>(); } @Override @@ -54,7 +49,6 @@ public void putAll(Map m) { // Need to reset or switch between on/off to conditional doesn't work featureManagement = new HashMap<>(); onOff = new HashMap<>(); - dynamicFeatures = new HashMap<>(); Map features = removePrefixes(m, "featureManagement"); @@ -100,18 +94,13 @@ private void addToFeatures(Map features, Str onOff.put(combined + key, (Boolean) featureValue); } else { Feature feature = null; - DynamicFeature dynamicFeature = null; try { feature = MAPPER.convertValue(featureValue, Feature.class); - dynamicFeature = MAPPER.convertValue(featureValue, DynamicFeature.class); } catch (IllegalArgumentException e) { LOGGER.error("Found invalid feature {} with value {}.", combined + key, featureValue.toString()); } // When coming from a file "feature.flag" is not a possible flag name - if (dynamicFeature != null && StringUtils.hasText(dynamicFeature.getAssigner()) - && dynamicFeature.getVariants().size() > 0) { - dynamicFeatures.put(key, dynamicFeature); - } else if (feature != null && feature.getEnabledFor() == null && feature.getKey() == null) { + if (feature != null && feature.getEnabledFor() == null && feature.getKey() == null) { if (LinkedHashMap.class.isAssignableFrom(featureValue.getClass())) { features = (LinkedHashMap) featureValue; for (String fKey : features.keySet()) { @@ -141,11 +130,4 @@ public Map getOnOff() { return onOff; } - /** - * @return the dynamicFeatures - */ - public Map getDynamicFeatures() { - return dynamicFeatures; - } - } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/DynamicFeature.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/DynamicFeature.java deleted file mode 100644 index 10db18a85d92b..0000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/DynamicFeature.java +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager.implementation.models; - -import java.util.HashMap; -import java.util.Map; - -import com.azure.spring.cloud.feature.manager.models.FeatureVariant; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * Defines the assigner and variants of a Dynamic Feature - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class DynamicFeature { - - private String assigner; - - private Map variants = new HashMap<>(); - - /** - * @return the assigner - */ - public String getAssigner() { - return assigner; - } - - /** - * @param assigner the assigner to set - */ - public void setAssigner(String assigner) { - this.assigner = assigner; - } - - /** - * @return the variants - */ - public Map getVariants() { - return variants; - } - - /** - * @param variants the variants to set - */ - public void setVariants(Map variants) { - this.variants = variants; - } -} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerTest.java deleted file mode 100644 index 6320c1dda0a31..0000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerTest.java +++ /dev/null @@ -1,272 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.when; - -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Stream; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.ApplicationContext; - -import com.azure.spring.cloud.feature.manager.implementation.FeatureManagementConfigProperties; -import com.azure.spring.cloud.feature.manager.implementation.FeatureManagementProperties; -import com.azure.spring.cloud.feature.manager.implementation.models.DynamicFeature; -import com.azure.spring.cloud.feature.manager.models.FeatureDefinition; -import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; -import com.azure.spring.cloud.feature.manager.models.FeatureVariant; -import com.azure.spring.cloud.feature.manager.models.IFeatureFilter; -import com.azure.spring.cloud.feature.manager.models.IFeatureVariantAssigner; -import com.azure.spring.cloud.feature.manager.testobjects.DiscountBanner; -import com.azure.spring.cloud.feature.manager.testobjects.MockableProperties; - -import reactor.core.publisher.Mono; - -/** - * Unit tests for FeatureManager. - */ -@SpringBootTest(classes = { TestConfiguration.class, SpringBootTest.class }) -public class DynamicFeatureManagerTest { - - private static final String USERS = "users"; - - private static final String GROUPS = "groups"; - - private static final String DEFAULT_ROLLOUT_PERCENTAGE = "defaultRolloutPercentage"; - - private static final LinkedHashMap EMPTY_MAP = new LinkedHashMap<>(); - - private DynamicFeatureManager featureManager; - - @Mock - private ApplicationContext context; - - @Mock - private FeatureManagementConfigProperties properties; - - @Mock - private FeatureManagementProperties featureManagementPropertiesMock; - - @Mock - private MockFilter filterMock; - - @Mock - private MockableProperties variantProperties; - - @Mock - private ObjectProvider propertiesProviderMock; - - @Mock - private Stream streamMock; - - @Mock - private Stream filterStreamMock; - - @BeforeEach - public void setup() { - MockitoAnnotations.openMocks(this); - when(properties.isFailFast()).thenReturn(true); - - DiscountBanner discountBannerBig = new DiscountBanner(); - discountBannerBig.setColor("#DDD"); - discountBannerBig.setSize(400); - DiscountBanner discountBannerSmall = new DiscountBanner(); - discountBannerSmall.setColor("#999"); - discountBannerSmall.setSize(150); - - Map discountBannerMap = new HashMap<>(); - - discountBannerMap.put("Big", discountBannerBig); - discountBannerMap.put("Small", discountBannerSmall); - - when(variantProperties.getDiscountBanner()).thenReturn(discountBannerMap); - when(propertiesProviderMock.stream()).thenReturn(streamMock); - when(streamMock.filter(Mockito.any())).thenReturn(filterStreamMock); - - featureManager = new DynamicFeatureManager(context, propertiesProviderMock, featureManagementPropertiesMock); - } - - @AfterEach - public void cleanup() throws Exception { - MockitoAnnotations.openMocks(this).close(); - } - - @Test - public void getVariantAsyncDefaultBasic() { - when(context.getBean(Mockito.matches("Test.Assigner"))).thenReturn(filterMock); - - DynamicFeature dynamicFeature = new DynamicFeature(); - dynamicFeature.setAssigner("Test.Assigner"); - - Map variants = new LinkedHashMap<>(); - - variants.put("0", createFeatureVariant("DiscountBanner.Big", EMPTY_MAP, EMPTY_MAP, 100)); - variants.get("0").setDefault(true); - dynamicFeature.setVariants(variants); - - when(filterMock.assignVariantAsync(Mockito.any())).thenReturn(Mono.just(variants.get("0"))); - - Map params = new LinkedHashMap<>(); - params.put("DiscountBanner", dynamicFeature); - - Optional optionalProp = Optional.of(variantProperties); - - when(featureManagementPropertiesMock.getDynamicFeatures()).thenReturn(params); - when(filterStreamMock.findFirst()).thenReturn(optionalProp); - - DiscountBanner testObject = featureManager.getVariantAsync("DiscountBanner", DiscountBanner.class) - .block(); - - assertNotNull(testObject); - assertEquals(400, testObject.getSize()); - } - - @Test - public void getVariantNoDefault() { - DynamicFeature dynamicFeature = new DynamicFeature(); - dynamicFeature.setAssigner("Test.Assigner"); - - Map variants = new LinkedHashMap<>(); - - variants.put("0", createFeatureVariant("DiscountBanner.Big", EMPTY_MAP, EMPTY_MAP, 100)); - - dynamicFeature.setVariants(variants); - - Map params = new LinkedHashMap<>(); - params.put("DiscountBanner", dynamicFeature); - - Optional optionalProp = Optional.of(variantProperties); - - when(featureManagementPropertiesMock.getDynamicFeatures()).thenReturn(params); - when(filterStreamMock.findFirst()).thenReturn(optionalProp); - - FeatureManagementException e = assertThrows(FeatureManagementException.class, - () -> featureManager.getVariantAsync("DiscountBanner", Object.class).block()); - assertEquals("A default variant cannot be found for the feature DiscountBanner", e.getMessage()); - } - - @Test - public void getVariantAsyncNonDefault() throws FilterNotFoundException, FeatureManagementException { - FeatureVariant variant = createFeatureVariant("DiscountBanner.Small", EMPTY_MAP, EMPTY_MAP, 100); - - when(context.getBean(Mockito.matches("Test.Assigner"))).thenReturn(filterMock); - when(filterMock.assignVariantAsync(Mockito.any())).thenReturn(Mono.just(variant)); - - DynamicFeature dynamicFeature = new DynamicFeature(); - dynamicFeature.setAssigner("Test.Assigner"); - - Map variants = new LinkedHashMap<>(); - - variants.put("0", createFeatureVariant("DiscountBanner.Big", EMPTY_MAP, EMPTY_MAP, 0)); - variants.get("0").setDefault(true); - variants.put("1", variant); - dynamicFeature.setVariants(variants); - - Map params = new LinkedHashMap<>(); - params.put("DiscountBanner", dynamicFeature); - - Optional optionalProp = Optional.of(variantProperties); - - when(featureManagementPropertiesMock.getDynamicFeatures()).thenReturn(params); - when(filterStreamMock.findFirst()).thenReturn(optionalProp); - - DiscountBanner testObject = featureManager.getVariantAsync("DiscountBanner", DiscountBanner.class) - .block(); - assertNotNull(testObject); - assertEquals(150, testObject.getSize()); - } - - @Test - public void featureWithoutAName() { - IllegalArgumentException e = assertThrows(IllegalArgumentException.class, - () -> featureManager.getVariantAsync("", DiscountBanner.class).block()); - assertThat(e).hasMessage("Feature Variant name can not be empty or null."); - } - - @Test - public void featureAssignerNotFound() { - FeatureVariant variant = createFeatureVariant("DiscountBanner.Small", EMPTY_MAP, EMPTY_MAP, 100); - - when(context.getBean(Mockito.matches("FeatureAssignerThatDoesntExist"))) - .thenThrow(new NoSuchBeanDefinitionException("")); - when(filterMock.assignVariantAsync(Mockito.any())).thenReturn(Mono.just(variant)); - - DynamicFeature dynamicFeature = new DynamicFeature(); - dynamicFeature.setAssigner("FeatureAssignerThatDoesntExist"); - - Map variants = new LinkedHashMap<>(); - - variants.put("0", createFeatureVariant("DiscountBanner.Big", EMPTY_MAP, EMPTY_MAP, 0)); - variants.get("0").setDefault(true); - variants.put("1", variant); - dynamicFeature.setVariants(variants); - - Map params = new LinkedHashMap<>(); - params.put("FeatureAssignerThatDoesntExist", dynamicFeature); - - Optional optionalProp = Optional.of(variantProperties); - - when(featureManagementPropertiesMock.getDynamicFeatures()).thenReturn(params); - when(filterStreamMock.findFirst()).thenReturn(optionalProp); - - FeatureManagementException e = assertThrows(FeatureManagementException.class, - () -> featureManager.getVariantAsync("FeatureAssignerThatDoesntExist", DiscountBanner.class).block()); - assertThat(e).hasMessage( - "The feature variant assigner FeatureAssignerThatDoesntExist specified for feature FeatureAssignerThatDoesntExist was not found."); - } - - class MockFilter implements IFeatureFilter, IFeatureVariantAssigner { - - @Override - public Mono assignVariantAsync(FeatureDefinition featureDefinition) { - return null; - } - - @Override - public boolean evaluate(FeatureFilterEvaluationContext context) { - return false; - } - - } - - private FeatureVariant createFeatureVariant(String variantName, LinkedHashMap users, - LinkedHashMap groups, int defautPercentage) { - return createFeatureVariant(variantName, "", users, groups, defautPercentage); - } - - private FeatureVariant createFeatureVariant(String variantName, String additionalRerence, - LinkedHashMap users, - LinkedHashMap groups, int defautPercentage) { - FeatureVariant variant = new FeatureVariant(); - variant.setName(variantName); - variant.setDefault(false); - variant.setConfigurationReference(variantName + additionalRerence); - - LinkedHashMap parameters = new LinkedHashMap(); - - parameters.put(USERS, users); - parameters.put(GROUPS, groups); - parameters.put(DEFAULT_ROLLOUT_PERCENTAGE, defautPercentage); - - variant.setAssignmentParameters(parameters); - - return variant; - } - -} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementPropertiesTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementPropertiesTest.java deleted file mode 100644 index 395ab52118cba..0000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementPropertiesTest.java +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager.implementation; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.concurrent.ExecutionException; - -import org.junit.jupiter.api.Test; - -import com.azure.spring.cloud.feature.manager.FilterNotFoundException; -import com.azure.spring.cloud.feature.manager.implementation.models.DynamicFeature; -import com.azure.spring.cloud.feature.manager.implementation.models.Feature; -import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; -import com.azure.spring.cloud.feature.manager.models.FeatureVariant; - -public class FeatureManagementPropertiesTest { - - private static final String FEATURE_KEY = "TestFeature"; - - private static final String FILTER_NAME = "Filter1"; - - private static final String PARAM_1_NAME = "param1"; - - private static final String PARAM_1_VALUE = "testParam"; - - /** - * Tests the conversion that takes place when data comes from EnumerablePropertySource. - */ - @Test - public void loadFeatureManagerWithLinkedHashSet() { - Feature f = new Feature(); - f.setKey(FEATURE_KEY); - - LinkedHashMap testMap = new LinkedHashMap(); - LinkedHashMap testFeature = new LinkedHashMap(); - LinkedHashMap enabledFor = new LinkedHashMap(); - LinkedHashMap ffec = new LinkedHashMap(); - LinkedHashMap parameters = new LinkedHashMap(); - ffec.put("name", FILTER_NAME); - parameters.put(PARAM_1_NAME, PARAM_1_VALUE); - ffec.put("parameters", parameters); - enabledFor.put("0", ffec); - testFeature.put("enabled-for", enabledFor); - testMap.put(f.getKey(), testFeature); - - FeatureManagementProperties properties = new FeatureManagementProperties(); - properties.putAll(testMap); - assertNotNull(properties.getFeatureManagement()); - assertEquals(1, properties.getFeatureManagement().size()); - assertNotNull(properties.getFeatureManagement().get(FEATURE_KEY)); - Feature feature = properties.getFeatureManagement().get(FEATURE_KEY); - assertEquals(FEATURE_KEY, feature.getKey()); - assertEquals(1, feature.getEnabledFor().size()); - FeatureFilterEvaluationContext zeroth = feature.getEnabledFor().get(0); - assertEquals(FILTER_NAME, zeroth.getName()); - assertEquals(1, zeroth.getParameters().size()); - assertEquals(PARAM_1_VALUE, zeroth.getParameters().get(PARAM_1_NAME)); - } - - @Test - public void isEnabledPeriodSplit() throws InterruptedException, ExecutionException, FilterNotFoundException { - LinkedHashMap features = new LinkedHashMap<>(); - LinkedHashMap featuresOn = new LinkedHashMap<>(); - - featuresOn.put("A", true); - features.put("Beta", featuresOn); - - FeatureManagementProperties properties = new FeatureManagementProperties(); - properties.putAll(features); - - assertTrue(properties.getOnOff().get("Beta.A")); - } - - @Test - public void isEnabledInvalid() throws InterruptedException, ExecutionException, FilterNotFoundException { - LinkedHashMap features = new LinkedHashMap(); - LinkedHashMap featuresOn = new LinkedHashMap(); - - featuresOn.put("A", 5); - features.put("Beta", featuresOn); - - FeatureManagementProperties properties = new FeatureManagementProperties(); - properties.putAll(features); - - assertNull(properties.getOnOff().getOrDefault("Beta.A", null)); - assertEquals(0, properties.size()); - } - - @Test - public void bootstrapConfiguration() { - HashMap features = new HashMap(); - features.put("FeatureU", false); - Feature featureV = new Feature(); - HashMap filterMapper = new HashMap(); - - FeatureFilterEvaluationContext enabledFor = new FeatureFilterEvaluationContext(); - enabledFor.setName("Random"); - - LinkedHashMap parameters = new LinkedHashMap(); - parameters.put("chance", "50"); - - enabledFor.setParameters(parameters); - filterMapper.put(0, enabledFor); - featureV.setEnabledFor(filterMapper); - features.put("FeatureV", featureV); - - FeatureManagementProperties properties = new FeatureManagementProperties(); - properties.putAll(features); - - assertNotNull(properties.getOnOff()); - assertNotNull(properties.getFeatureManagement()); - - assertEquals(properties.getOnOff().get("FeatureU"), false); - Feature feature = properties.getFeatureManagement().get("FeatureV"); - assertEquals(feature.getEnabledFor().size(), 1); - FeatureFilterEvaluationContext ffec = feature.getEnabledFor().get(0); - assertEquals(ffec.getName(), "Random"); - assertEquals(ffec.getParameters().size(), 1); - assertEquals(ffec.getParameters().get("chance"), "50"); - } - - @Test - public void featureVariantLoadTest() { - HashMap features = new HashMap(); - - DynamicFeature df = new DynamicFeature(); - df.setAssigner("Microsoft.Targeting"); - - FeatureVariant fv = new FeatureVariant(); - fv.setName("TestVariant"); - fv.setDefault(true); - fv.setConfigurationReference("config.reference"); - - LinkedHashMap assignmentParameters = new LinkedHashMap<>(); - - assignmentParameters.put("User", "Doe"); - - fv.setAssignmentParameters(assignmentParameters); - - Map variants = new HashMap<>(); - - variants.put("TestVariant", fv); - - df.setVariants(variants); - - features.put("TestDynamicFeature", df); - - FeatureManagementProperties properties = new FeatureManagementProperties(); - properties.putAll(features); - - assertNotNull(properties.getDynamicFeatures().get("TestDynamicFeature")); - } -} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/MockableProperties.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/MockableProperties.java deleted file mode 100644 index cd5d2cedc01a5..0000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/manager/testobjects/MockableProperties.java +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager.testobjects; - -import java.util.Map; - -import com.azure.spring.cloud.feature.manager.IDynamicFeatureProperties; - -public class MockableProperties implements IDynamicFeatureProperties { - - private Map discountBanner; - - /** - * @return the discountBanner - */ - public Map getDiscountBanner() { - return discountBanner; - } - - /** - * @param discountBanner the discountBanner to set - */ - public void setDiscountBanner(Map discountBanner) { - this.discountBanner = discountBanner; - } - -} From c612103b151ba89f6b6ee0727874346c21002064 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 6 Feb 2023 13:58:44 -0800 Subject: [PATCH 04/38] Fixing Dependencies, and Dynamic Web --- .../src/main/resources/revapi/revapi.json | 10 +++ eng/versioning/version_client.txt | 4 +- .../pom.xml | 28 ++----- .../DynamicFeatureManagerSnapshot.java | 59 -------------- .../FeatureManagementWebConfiguration.java | 14 ---- .../DynamicFeatureManagerSnapshotTest.java | 80 ------------------- .../pom.xml | 45 +++++++---- .../pom.xml | 2 +- .../pom.xml | 4 +- 9 files changed, 50 insertions(+), 196 deletions(-) delete mode 100644 sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshot.java delete mode 100644 sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshotTest.java diff --git a/eng/code-quality-reports/src/main/resources/revapi/revapi.json b/eng/code-quality-reports/src/main/resources/revapi/revapi.json index 305256c209e47..17a3c5566ac96 100644 --- a/eng/code-quality-reports/src/main/resources/revapi/revapi.json +++ b/eng/code-quality-reports/src/main/resources/revapi/revapi.json @@ -776,6 +776,16 @@ "code": "java.class.externalClassExposedInAPI", "new": "class com.azure.data.appconfiguration.ConfigurationAsyncClient", "justification": "This is an Azure SDK class that is used in the public API." + }, + { + "code": "java.class.externalClassExposedInAPI", + "new": "class com.azure.spring.cloud.feature.manager.FeatureManager", + "justification": "This is an Azure SDK class that is used in the public API." + }, + { + "code": "java.class.externalClassExposedInAPI", + "new": "class com.azure.spring.cloud.feature.manager.FilterNotFoundException", + "justification": "This is an Azure SDK class that is used in the public API." } ] } diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 517cd1833d942..5ee03cfa18e0d 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -179,8 +179,8 @@ com.azure:azure-developer-loadtesting;1.0.0-beta.2;1.0.0-beta.3 com.azure:azure-identity-extensions;1.1.0;1.2.0-beta.1 com.azure.spring:spring-cloud-azure-appconfiguration-config-web;2.11.0;4.0.0-beta.1 com.azure.spring:spring-cloud-azure-appconfiguration-config;2.11.0;4.0.0-beta.1 -com.azure.spring:spring-cloud-azure-feature-management-web;2.10.0;4.0.0-beta.1 -com.azure.spring:spring-cloud-azure-feature-management;2.10.0;4.0.0-beta.1 +com.azure.spring:spring-cloud-azure-feature-management-web;2.10.0;4.0.0-beta.3 +com.azure.spring:spring-cloud-azure-feature-management;2.10.0;4.0.0-beta.3 com.azure.spring:spring-cloud-azure-starter-appconfiguration-config;2.11.0;4.0.0-beta.1 com.azure.spring:spring-cloud-azure-dependencies;4.5.0;4.6.0-beta.1 com.azure.spring:spring-messaging-azure;4.5.0;4.6.0-beta.1 diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml index 45f7a2e6f87fc..e156496d4ef1a 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml @@ -29,12 +29,12 @@ org.springframework.boot spring-boot-starter-web - 2.7.4 + 2.7.8 org.springframework.boot spring-boot-starter-actuator - 2.7.4 + 2.7.8 true @@ -43,24 +43,6 @@ 3.1.2 true - - org.junit.vintage - junit-vintage-engine - 5.8.2 - test - - - org.hamcrest - hamcrest-core - - - - - junit - junit - 4.13.2 - test - org.mockito mockito-core @@ -70,7 +52,7 @@ org.springframework.boot spring-boot-starter-test - 2.7.4 + 2.7.8 test @@ -85,8 +67,8 @@ - org.springframework.boot:spring-boot-starter-actuator:[2.7.4] - org.springframework.boot:spring-boot-starter-web:[2.7.4] + org.springframework.boot:spring-boot-starter-actuator:[2.7.8] + org.springframework.boot:spring-boot-starter-web:[2.7.8] org.springframework.cloud:spring-cloud-bus:[3.1.2] diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshot.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshot.java deleted file mode 100644 index e075d89c9223b..0000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshot.java +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager; - -import java.util.HashMap; -import java.util.Map; - -import reactor.core.publisher.Mono; - -/** - * Holds information on Feature Management properties and can check if a given feature is enabled. Returns the same - * value in the same request. - */ -public class DynamicFeatureManagerSnapshot { - - private final DynamicFeatureManager dynamicFeatureManager; - - private final Map requestMap; - - /** - * Used to evaluate whether a feature is enabled or disabled. When setup with the @RequestScope it will - * return the same value for all checks of the given feature flag. - * - * @param dynamicFeatureManager DynamicFeatureManager - */ - public DynamicFeatureManagerSnapshot(DynamicFeatureManager dynamicFeatureManager) { - this.dynamicFeatureManager = dynamicFeatureManager; - this.requestMap = new HashMap<>(); - } - - /** - * Returns a feature variant of the type given. - *

- * If getVariantAsync has all ready been called on this variant, it will return the same variant as it did before. - * - * @param Type of the feature that will be returned. - * @param variantName name of the variant being checked. - * @param type Type of the feature being checked. - * @return variant of the provided type, can return Mono with a null value if the feature requested doesn't match - * the stored type. - * @throws FeatureManagementException A generic exception for when something goes wrong with the variant generation - */ - public Mono getVariantAsync(String variantName, Class type) - throws FeatureManagementException { - if (!requestMap.containsKey(variantName)) { - return dynamicFeatureManager.getVariantAsync(variantName, type).doOnSuccess(newVariant -> { - requestMap.put(variantName, newVariant); - }); - } - - T variant = null; - Object o = requestMap.get(variantName); - if (o.getClass().equals(type)) { - variant = type.cast(o); - } - - return Mono.justOrEmpty(variant); - } -} diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementWebConfiguration.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementWebConfiguration.java index 0f9169271e1b2..4ae1f5301095a 100644 --- a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementWebConfiguration.java +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementWebConfiguration.java @@ -9,8 +9,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.web.context.annotation.RequestScope; -import com.azure.spring.cloud.feature.manager.DynamicFeatureManager; -import com.azure.spring.cloud.feature.manager.DynamicFeatureManagerSnapshot; import com.azure.spring.cloud.feature.manager.FeatureHandler; import com.azure.spring.cloud.feature.manager.FeatureManager; import com.azure.spring.cloud.feature.manager.FeatureManagerSnapshot; @@ -36,18 +34,6 @@ public FeatureManagerSnapshot featureManagerSnapshot(FeatureManager featureManag return new FeatureManagerSnapshot(featureManager); } - /** - * Creates DynamicFeatureManagerSnapshot - * - * @param dynamicFeatureManager App Configuration Dynamic Feature Manager - * @return DynamicFeatureManagerSnapshot - */ - @Bean - @RequestScope - public DynamicFeatureManagerSnapshot dynamicFeatureManagerSnapshot(DynamicFeatureManager dynamicFeatureManager) { - return new DynamicFeatureManagerSnapshot(dynamicFeatureManager); - } - /** * Creates FeatureHandler * diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshotTest.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshotTest.java deleted file mode 100644 index 32ea92f2faedd..0000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management-web/src/test/java/com/azure/spring/cloud/feature/manager/DynamicFeatureManagerSnapshotTest.java +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.concurrent.ExecutionException; - -import javax.servlet.http.HttpServletRequest; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInfo; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import reactor.core.publisher.Mono; - -public class DynamicFeatureManagerSnapshotTest { - - @Mock - DynamicFeatureManager dynamicFeatureManager; - - @Mock - HttpServletRequest request; - - @InjectMocks - DynamicFeatureManagerSnapshot dynamicFeatureManagerSnapshot; - - @BeforeEach - public void setup(TestInfo testInfo) { - MockitoAnnotations.openMocks(this); - } - - @Test - public void initialLoad() - throws InterruptedException, ExecutionException, FilterNotFoundException, FeatureManagementException { - when(dynamicFeatureManager.getVariantAsync(Mockito.matches("myVariant"), Mockito.any())) - .thenReturn(Mono.just(new Object())); - - Object returnValue = dynamicFeatureManagerSnapshot.getVariantAsync("myVariant", Object.class).block(); - assertNotNull(returnValue); - verify(dynamicFeatureManager, times(1)).getVariantAsync("myVariant", Object.class); - } - - @Test - public void initialAlreadyExists() throws InterruptedException, ExecutionException, FilterNotFoundException, FeatureManagementException { - when(dynamicFeatureManager.getVariantAsync(Mockito.matches("exitingVariant"), Mockito.any())) - .thenReturn(Mono.just(new Object())); - - Object returnValue = dynamicFeatureManagerSnapshot.getVariantAsync("exitingVariant", Object.class).block(); - assertNotNull(returnValue); - verify(dynamicFeatureManager, times(1)).getVariantAsync("exitingVariant", Object.class); - - returnValue = dynamicFeatureManagerSnapshot.getVariantAsync("exitingVariant", Object.class).block(); - assertNotNull(returnValue); - verify(dynamicFeatureManager, times(1)).getVariantAsync("exitingVariant", Object.class); - } - - @Test - public void invalidType() - throws InterruptedException, ExecutionException, FilterNotFoundException, FeatureManagementException { - when(dynamicFeatureManager.getVariantAsync(Mockito.matches("exitingVariant"), Mockito.any())) - .thenReturn(Mono.just(1)); - - Object returnValue = dynamicFeatureManagerSnapshot.getVariantAsync("exitingVariant", Integer.class).block(); - assertEquals(1, returnValue); - returnValue = dynamicFeatureManagerSnapshot.getVariantAsync("exitingVariant", String.class).block(); - assertNull(returnValue); - verify(dynamicFeatureManager, times(1)).getVariantAsync("exitingVariant", Integer.class); - } - -} diff --git a/sdk/spring/spring-cloud-azure-feature-management/pom.xml b/sdk/spring/spring-cloud-azure-feature-management/pom.xml index b7647e7c7e450..c457d185bbcd7 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/pom.xml +++ b/sdk/spring/spring-cloud-azure-feature-management/pom.xml @@ -1,4 +1,6 @@ - + com.azure azure-client-sdk-parent @@ -9,7 +11,8 @@ com.azure.spring spring-cloud-azure-feature-management - 4.0.0-beta.3 + 4.0.0-beta.3 Spring Cloud Azure Feature Management Adds Feature Management into Spring @@ -30,32 +33,38 @@ org.springframework spring-context - 5.3.23 + 5.3.25 org.springframework.boot spring-boot-starter - 2.7.4 + 2.7.8 com.fasterxml.jackson.core jackson-annotations - 2.13.4 + 2.13.4 com.fasterxml.jackson.core jackson-databind - 2.13.4 + 2.13.4.2 io.projectreactor.netty reactor-netty - 1.0.23 + 1.0.27 org.springframework.boot spring-boot-starter-test - 2.7.4 + 2.7.8 test @@ -64,16 +73,22 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.0.0-M3 + 3.0.0-M3 - com.fasterxml.jackson.core:jackson-annotations:[2.13.4] - com.fasterxml.jackson.core:jackson-databind:[2.13.4] - io.projectreactor.netty:reactor-netty:[1.0.23] - org.springframework.boot:spring-boot-starter:[2.7.4] - org.springframework:spring-context:[5.3.23] + com.fasterxml.jackson.core:jackson-annotations:[2.13.4] + com.fasterxml.jackson.core:jackson-databind:[2.13.4.2] + io.projectreactor.netty:reactor-netty:[1.0.27] + org.springframework.boot:spring-boot-starter:[2.7.8] + org.springframework:spring-context:[5.3.25] @@ -81,4 +96,4 @@ - \ No newline at end of file + diff --git a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml index 8bd1a72566e24..57de676187d73 100644 --- a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/pom.xml @@ -26,7 +26,7 @@ com.azure.spring spring-cloud-azure-feature-management-web - 4.0.0-beta.1 diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml index d55776fe9521a..9db673aaf1aa9 100644 --- a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml @@ -8,7 +8,7 @@ org.springframework.boot spring-boot-starter-parent - 2.7.7 + 2.7.8 @@ -38,7 +38,7 @@ org.springframework.boot spring-boot-starter-test - 2.7.7 + 2.7.8 test From c7bbe41e6855638c400c4eb08c98e7c0c7e2e8ad Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 6 Feb 2023 15:25:34 -0800 Subject: [PATCH 05/38] Updating Readmes and dependencies --- .../pom.xml | 12 +- .../pom.xml | 31 +-- .../README.md | 214 +----------------- .../README.md | 63 +++--- 4 files changed, 44 insertions(+), 276 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index dfde10e33a458..43e5ea247a0e6 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -61,7 +61,7 @@ com.azure azure-core - 1.35.0 + 1.36.0 com.azure @@ -71,7 +71,7 @@ com.azure azure-identity - 1.7.3 + 1.8.0 com.azure @@ -81,7 +81,7 @@ com.azure azure-core-http-netty - 1.12.8 + 1.13.0 org.hibernate.validator @@ -96,12 +96,12 @@ com.azure.spring spring-cloud-azure-service - 4.4.1 + 4.5.0 com.azure.spring spring-cloud-azure-actuator-autoconfigure - 4.4.1 + 4.5.0 @@ -173,4 +173,4 @@ - \ No newline at end of file + diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml b/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml index 51a5030b3bca6..11819b2783dea 100644 --- a/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml +++ b/sdk/spring/spring-cloud-azure-feature-management-web/pom.xml @@ -27,21 +27,15 @@ - - org.springframework.boot - spring-boot-starter-test - 2.7.4 - test - org.springframework spring-web - 5.3.23 + 5.3.25 org.springframework spring-webmvc - 5.3.23 + 5.3.25 javax.servlet @@ -54,19 +48,10 @@ spring-cloud-azure-feature-management 4.0.0-beta.3 - - com.google.code.findbugs - jsr305 - 3.0.2 - provided - - - junit - junit - 4.13.2 + org.springframework.boot + spring-boot-starter-test + 2.7.8 test @@ -82,8 +67,8 @@ com.azure.spring:spring-cloud-azure-feature-management:[4.0.0-beta.3] javax.servlet:javax.servlet-api:[4.0.1] - org.springframework:spring-web:[5.3.23] - org.springframework:spring-webmvc:[5.3.23] + org.springframework:spring-web:[5.3.25] + org.springframework:spring-webmvc:[5.3.25] @@ -91,4 +76,4 @@ - \ No newline at end of file + diff --git a/sdk/spring/spring-cloud-azure-feature-management/README.md b/sdk/spring/spring-cloud-azure-feature-management/README.md index 412c116dae643..6085c683a1080 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/README.md +++ b/sdk/spring/spring-cloud-azure-feature-management/README.md @@ -115,7 +115,7 @@ public class DisabledFeaturesHandler implements IDisabledFeaturesHandler{ ### Routing -Certain routes may expose application capabilites that are gated by features. These routes can redirected if a feature is disabled to another endpoint. +Certain routes may expose application capabilities that are gated by features. These routes can redirected if a feature is disabled to another endpoint. ```java @GetMapping("/featureT") @@ -268,217 +268,5 @@ Options are available to customize how targeting evaluation is performed across } ``` -## Dynamic Features - -When new features are being added to an application there may come a time when a feature has multiple different proposed design options. A common pattern when this happens is to do some form of A/B testing. That is, provide a different version of the feature to different segments of the user base, and judge off user interaction which is better. The dynamic feature functionality contained in this library aims to proivde a simplistic, standardized method for developers to perform this form of A/B testing. - -In the scenario above, the different proposals for the design of a feature are referred to as variants of the feature. The feature itself is referred to as a dynamic feature. The variants of a dynamic feature can have types ranging from object, to string, to integer and so on. There is no limit to the amount of variants a dynamic feature may have. A developer is free to choose what type should be returned when a variant of a dynamic feature is requested. They are also free to choose how many variants are available to select from. - -Each variant of a dynamic feature is associated with a different configuration of the feature. Additionally, each variant of a dynamic feature contains information describing under what circumstances the variant should be used. - -### Consumption - -Dynamic Features are accessible through `DynamicFeatureManager`. - -The dynamic feature manager performs a resolution process that takes the name of a feature and returns a strongly typed value to represent the variant's value. - -The following steps are performed during the retrieval of a dynamic feature's variant - -1. Lookup the configuration of the specified dynamic feature to find the registered variants -1. Assign one of the registered variants to be used. -1. Resolve typed value based off of the assigned variant. - -The Dynamic Feature Manager is made available by using `@Autwired` on `DynamicFeatureManager` and calling it's `getVariantAsync` method. In addition any required feature variant assigners need to be generated as `@Component`, such as the `TargetingEvaluator`. - -**NOTE:** `TargetingEvaluator` extends `TargetingFilter` so it can be used for both at the same time. - -### Usage Example - -One possible example of when variants may be used is in a web application when there is a desire to test different visuals. In the following examples a mock of how one might assign different variants of a web page background to their users is shown. - -```java -@Autowired -private DynamicFeatureManager dynamicFeatureManager; - -... - -// -// Modify view based off multiple possible variants -model.setBackgroundUrl(dynamicFeatureManager.GetVariantAsync("HomeBackground", String.class).block()); -``` - -### Dynamic Feature Declaration - -Dynamic features can be configured in a configuration file similarly to feature flags. Instead of being defined in the `FeatureManagement.FeatureFlags` section, they are defined in the `FeatureManagement.DynamicFeatures` section. Additionally, dynamic features have the following properties. - -* Assigner: The assigner that should be used to select which variant should be used any time this feature is accessed. -* Variants: The different variants of the dynamic feature. - * Name: The name of the variant. - * Default: Whether the variant should be used if no variant could be explicitly assigned. One and only one default variant is required. - * ConfigurationReference: A reference to the configuration of the variant to be used as typed options in the application. - * AssignmentParameters: The parameters used in the assignment process to determine if this variant should be used. - -An example of a dynamic feature named "ShoppingCart" is shown below. - -```yml -feature-management: - dynamic-features: - ShoppingCart: - assigner: Microsoft.Targeting - variants: - - default: true - name: Big - configuration-reference: ShoppingCart.Big - assignment-parameters: - audience: - users: - - Alec - groups: [] - - name: Small - configuration-reference: ShoppingCart.Small - assignment-parameters: - audience: - users: [] - groups: - - name: Ring1 - rollout-percentage: 50 - default-rollout-percentage: 30 -feature-variants: - ShoppingCart: - Big: - Size: 400 - Color: green - Small: - Size: 150 - Color: gray -``` - -In the example above we see the declaration of a dynamic feature in a json configuration file. The dynamic feature is defined in the `feature-management.dynamic-features` section of configuration. The name of this dynamic feature is `ShoppingCart`. A dynamic feature must declare a feature variant assigner that should be used to select a variant when requested. In this case the built-in `Microsoft.Targeting` feature variant assigner is used. The dynamic feature has two different variants that are available to the application. One variant is named `Big` and the other is named `Small`. Each variant contains a configuration reference denoted by the `configuration-reference` property. The configuration reference is a pointer to a section of application configuration that contains the options that should be used for that variant. The variant also contains assignment parameters denoted by the `assignment-parameters` property. The assignment parameters are used by the assigner associated with the dynamic feature. The assigner reads the assignment parameters at run time when a variant of the dynamic feature is requested to choose which variant should be returned. - -An application that is configured with this `ShoppingCart` dynamic feature may request the value of a variant of the feature at runtime through the use of `DynamicFeatureManager.getVariantAsync`. The dynamic feature uses targeting for [variant assignment](#feature-variant-assignment) so each of the variants' assignment parameters specify a target audience that should receive the variant. For a walkthrough of how the targeting assigner would choose a variant in this scenario reference the [Microsoft.Targeting Assigner](#microsofttargeting-feature-variant-assigner) section. When the feature manager chooses one of the variants it resolves the value of the variant by resolving the configuration reference declared in the variant. The example above includes the configuration that is referenced by the `configuration-reference` of each variant. - -### Feature Variant Assigners - -A feature variant assigner is a component that uses contextual information within an application to decide which feature variant should be chosen when a variant of a dynamic feature is requested. - -### Feature Variant Assignment - -When requesting the value of a dynamic feature, the feature manager needs to determine which variant to use. The act of choosing which of the variants to be used is called "assignment." A built-in method of assignment allows the variants of a dynamic feature to be assigned to segments of an application's audience. This is the same [targeting](#microsofttargeting-feature-variant-assigner) strategy used by the targeting feature filter. - -To perform assignments, the feature manager uses components known as feature variant assigners. Feature variant assigners choose which of the variants of a dynamic feature should be assigned when a dynamic feature is requested. Each variant of a dynamic feature defines assignment parameters so that when an assigner is invoked, the assigner can tell under which conditions each variant should be selected. It is possible that an assigner is unable to choose between the list of available variants based on the configured assignment parameters. In this case, the feature manager chooses the **default variant**. The default variant is a variant that is marked explicitly as the default. It is required to have a default variant when configuring a dynamic feature in order to handle the possibility that an assigner is not able to select a variant of a dynamic feature. - -### Custom Assignment - -There may come a time when custom criteria is needed to decide which variant of a feature should be assigned when a feature is referenced. This is made possible by an extensibility model that allows the act of assignment to be overridden. Every feature registered in the feature management system that uses feature variants specifies what assigner should be used to choose a variant. - -```java -public interface IFeatureVariantAssigner extends IFeatureVariantAssignerMetadata { - - /** - * Assign a variant of a dynamic feature to be used based off of customized criteria. - * @param featureDefinition A variant assignment context that contains information needed to assign a variant for a - * dynamic feature. - * @return The variant that should be assigned for a given dynamic feature. - */ - public Mono assignVariantAsync(FeatureDefinition featureDefinition); - -} -``` - -### Built-In Feature Variant Assigners - -There is a built-in feature variant assigner that uses targeting. It comes with the `azure-spring-cloud-feature-management` package. This assigner is not added automatically, but it can be referenced and configured as a `@Component`. - -#### Microsoft.Targeting Feature Variant Assigner - -This feature variant assigner provides the capability to assign the variants of a dynamic feature to targeted audiences. An in-depth explanation of targeting is explained in the [targeting](#targeting) section. - -The assignment parameters used by the targeting feature variant assigner include an audience object which describes the user base that should receive the associated variant. The audience is made of users, groups, and a percentage of the entire user base. Each group object that is listed in the target audience is required to specify what percentage of the group's members should have receive the variant. If a user is specified in the users section directly, or if the user is in the included percentage of any of the group rollouts, or if the user falls into the default rollout percentage then that user will receive the associated variant. - -```yml -ShoppingCart: - assigner: Microsoft.Targeting - variants: - - default: true - name: Big - configuration-reference: ShoppingCart.Big - assignment-parameters: - audience: - users: - - - Alec - groups: - - - name: Ring0 - rollout-percentage: 100 - - - name: Ring1 - rollout-percentage: 50 - - name: Small - configuration-reference: ShoppingCart.Small - assignment-parameters: - audience: - users: - - Susan - groups: - - - name: Ring1 - rollout-percentage: 50 - default-rollout-percentage: 80 -``` - -Based on the configured audiences for the variants included in this feature, if the application is executed under the context of a user named `Alec` then the value of the `Big` variant will be returned. If the application is executing under the context of a user named `Susan` then the value of the `Small` variant will be returned. If a user match does not occur, then group matches are evaluated. If the application is executed under the context of a user in the group `Ring0` then the `Big` variant will be returned. If the user's group is `Ring1` instead, then the user has a 50% chance of being assigned to `Small`. If there is no user match nor group match, then the default rollout percentage is used. In this case, 80% of unmatched users will get the `Small` variant, leaving the other 20% to get the `Big` variant since it is marked as the Default. - -When using the targeting feature variant assigner, make sure to register it as well as an implementation of [ITargetingContextAccessor](#itargetingcontextaccessor). - -```java -@Component -@RequestScope -public class TargetingContextImpl implements ITargetingContextAccessor { - - @Autowired - private HttpServletRequest request; - - @Override - public Mono getContextAsync() { - ... - } - -} -``` - -### Variant Resolution - -When a variant of a dynamic feature has been chosen, the feature management system resolves the configuration reference associated with that variant. The resolution is done through the `configuration-reference` property. In the "[Configuring a Dynamic Feature](#configuring-a-dynamic-feature)" section we see a dynamic feature named `ShoppingCart`. The first variant of the feature, is named "Big", and is being referenced in the feature variant as `ShoppingCart.Big` in the configuration reference. The referenced section is shown below. - -```yml -feature-variants: - ShoppingCart: - Big: - size: 400 - color: "green" -``` - -In this case, there is a `@ConfigurationProperties` that implements `IDynamicFeatureProperties` - -```java -@ConfigurationProperties(prefix = "feature-variants") -public class ApplicationProperties implements IDynamicFeatureProperties { - - private Map shoppingCart; - - public Map getShoppingCart() { - return shoppingCart; - } - - public void setShoppingCart(Map shoppingCart) { - this.shoppingCart = shoppingCart; - } - -} -``` - -`IDynamicFeatureProperties` flags `@ConfigurationProperties` as containing `FeatureVariants`. Multiple `@ConfigurationProperties` can have `IDynamicFeatureProperties`. The feature management system resolves the configuration reference by accessing the `@ConfigurationProperties` and getting the variant from the `Map`. - [example_project]: https://github.com/Azure-Samples/azure-spring-boot-samples/tree/tag_azure-spring-boot_3.6.0/appconfiguration/feature-management-web-sample diff --git a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md index 9b069ff2ac166..939ac2a5af79e 100644 --- a/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md +++ b/sdk/spring/spring-cloud-azure-starter-appconfiguration-config/README.md @@ -14,26 +14,26 @@ This package helps Spring Application to load properties from Azure Configuratio ### Include the package -There are two libraries that can be used azure-spring-cloud-appconfiguration-config and azure-spring-cloud-appconfiguration-config-web. There are two differences between them the first being the web version takes on spring-web as a dependency, and the web version has various methods for refreshing configurations on a watch interval when the application is active. For more information on refresh see the [Configuration Refresh](#configuration-refresh) section. +There are two libraries that can be used spring-cloud-azure-appconfiguration-config and spring-cloud-azure-appconfiguration-config-web. There are two differences between them the first being the web version takes on spring-web as a dependency, and the web version has various methods for refreshing configurations on a watch interval when the application is active. For more information on refresh see the [Configuration Refresh](#configuration-refresh) section. -[//]: # ({x-version-update-start;com.azure.spring:azure-spring-cloud-appconfiguration-config;current}) +[//]: # ({x-version-update-start;com.azure.spring:spring-cloud-azure-appconfiguration-config;current}) ```xml com.azure.spring - azure-spring-cloud-appconfiguration-config - 2.10.0 + spring-cloud-azure-appconfiguration-config + 4.0.0-beta.1 ``` [//]: # ({x-version-update-end}) or -[//]: # ({x-version-update-start;com.azure.spring:azure-spring-cloud-appconfiguration-config;current}) +[//]: # ({x-version-update-start;com.azure.spring:spring-cloud-azure-appconfiguration-config;current}) ```xml com.azure.spring - azure-spring-cloud-appconfiguration-config-web - 2.10.0 + spring-cloud-azure-appconfiguration-config-web + 4.0.0-beta.1 ``` [//]: # ({x-version-update-end}) @@ -52,6 +52,7 @@ Name | Description | Required | Default ---|---|---|--- spring.cloud.azure.appconfiguration.stores | List of configuration stores from which to load configuration properties | Yes | true spring.cloud.azure.appconfiguration.enabled | Whether enable spring-cloud-azure-appconfiguration-config or not | No | true +spring.cloud.azure.appconfiguration.client-id | Client id of the user assigned managed identity, only required when choosing to use user assigned managed identity on Azure | No | null spring.cloud.azure.appconfiguration.refresh-interval | Amount of time, of type Duration, configurations are stored before a check can occur. | No | null `spring.cloud.azure.appconfiguration.stores` is a list of stores, where each store follows the following format: @@ -70,7 +71,6 @@ Name | Description | Required | Default spring.cloud.azure.appconfiguration.stores[0].endpoint | When the endpoint of an App Configuration store is specified, a managed identity or a token credential provided using `AppConfigCredentialProvider` will be used to connect to the App Configuration service. An `IllegalArgumentException` will be thrown if the endpoint and connection-string are specified at the same time. | Conditional | null spring.cloud.azure.appconfiguration.stores[0].endpoints | When multiple replica endpoints of an App Configuration store are specified, a managed identity or a token credential provided using `AppConfigCredentialProvider` will be used to connect to the App Configuration service. Replica endpoints should be listed in priority order of connection. An `IllegalArgumentException` will be thrown if multiple authentication methods are provided. | Conditional | null spring.cloud.azure.appconfiguration.stores[0].connection-string | When the connection-string of an App Configuration store is specified, HMAC authentication will be used to connect to the App Configuration service. An `IllegalArgumentException` will be thrown if the endpoint and connection-string are specified at the same time. | Conditional | null -spring.cloud.azure.appconfiguration.stores[0].managed-identity.client-id | Client id of the user assigned managed identity, only required when choosing to use user assigned managed identity on Azure | No | null `spring.cloud.azure.appconfiguration.stores[0].monitoring` is a set of configurations dealing with refresh of configurations: @@ -91,10 +91,29 @@ spring.cloud.azure.appconfiguration.stores[0].monitoring.push-notification.secon Name | Description | Required | Default ---|---|---|--- spring.cloud.azure.appconfiguration.stores[0].feature-flags.enabled | Whether feature flags are loaded from the config store. | No | false -spring.cloud.azure.appconfiguration.stores[0].feature-flags.label-filter | The label used to indicate which feature flags will be loaded. | No | \0 +spring.cloud.azure.appconfiguration.stores[0].feature-flags.selects[0].key-filter | The key pattern used to indicate which feature flags will be loaded. | No | \0 +spring.cloud.azure.appconfiguration.stores[0].feature-flags.selects[0].label-filter | The label used to indicate which feature flags will be loaded. | No | \0 ### Advanced usage +#### Geo-Replication + +Each replica created has its dedicated endpoint. Geo-replication is enabled when `spring.cloud.azure.appconfiguration.stores[0].endpoints` is set with multiple endpoints. + +```properties +spring.cloud.azure.appconfiguration.stores[0].endpoints[0]= +spring.cloud.azure.appconfiguration.stores[0].endpoints[1]= +spring.cloud.azure.appconfiguration.stores[0].endpoints[2]= +``` + +As shown you can list your replica endpoints in the order of the most preferred to the least preferred endpoint. When the current endpoint isn't accessible, the provider library will fail over to a less preferred endpoint, but it will try to connect to the more preferred endpoints from time to time. When a more preferred endpoint becomes available, it will switch to it for future requests. + +Note: The failover may occur if the App Configuration provider observes the following conditions. +Receives responses with service unavailable status (HTTP status code 500 or above). +Experiences with network connectivity issues. +Requests are throttled (HTTP status code 429). +The failover won't happen for client errors like authentication failures. + #### Load from multiple configuration stores If the application needs to load configuration properties from multiple stores, following configuration sample describes how the bootstrap.properties(or .yaml) can be configured. @@ -121,7 +140,7 @@ Multiple labels can be separated with comma, if duplicate keys exists for multip #### Spring Profiles -Spring Profiles are supported by automatically by being set as App Configuration Labels. Using the label filter configuration overrides profile use. To include Spring Profiles and labels: +Spring Profiles are supported automatically by being set as the default label value of your selected keys. Using the label filter configuration overrides profile use. To include Spring Profiles and labels: ```properties spring.cloud.azure.appconfiguration.stores[0].selects[0].label-filter=${spring.profiles.active},v1 @@ -242,30 +261,6 @@ spring.cloud.azure.appconfiguration.stores[0].endpoint=[config-store-endpoint] spring.cloud.azure.appconfiguration.managed-identity.client-id=[client-id] ``` -#### Token Credential Provider - -Another method of authentication is using AppConfigCredentialProvider and/or KeyVaultCredentialProvider. By implementing either of these classes and providing and generating a @Bean of them will enable authentication through any method defined by the [Java Azure SDK][azure_identity_sdk]. The uri value is the endpoint/dns name of the connection service, so if needed different credentials can be used per config store/key vault. - -```java -public class MyCredentials implements AppConfigCredentialProvider, KeyVaultCredentialProvider { - - @Override - public TokenCredential getAppConfigCredential(String uri) { - return buildCredential(); - } - - @Override - public TokenCredential getKeyVaultCredential(String uri) { - return buildCredential(); - } - - TokenCredential buildCredential() { - return new DefaultAzureCredentialBuilder().build(); - } - -} -``` - #### Client Builder Customization The service client builders used for connecting to App Configuration and Key Vault can be customized by implementing interfaces `ConfigurationClientBuilderSetup` and `SecretClientBuilderSetup` respectively. Generating and providing a `@Bean` of them will update the default service client builders used in [App Configuration SDK][app_configuration_SDK] and [Key Vault SDK][key_vault_SDK]. If necessary, the customization can be done per App Configuration store or Key Vault instance. From 92df7f45cd6ab6ef1c4824ce54f550bc8c68e95b Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 6 Feb 2023 16:06:15 -0800 Subject: [PATCH 06/38] Fixing Links --- .../spring-cloud-azure-appconfiguration-config-web/README.md | 2 +- sdk/spring/spring-cloud-azure-appconfiguration-config/README.md | 2 +- sdk/spring/spring-cloud-azure-feature-management-web/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/README.md b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/README.md index dc6c4912a43fb..cb22d8358da31 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/README.md +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/README.md @@ -1,3 +1,3 @@ # Spring Cloud for Azure appconfiguration config web client library for Java -See: [Spring Cloud for Azure App Configuration Starter](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/appconfiguration/azure-spring-cloud-starter-appconfiguration-config) \ No newline at end of file +See: [Spring Cloud for Azure App Configuration Starter](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/azure-spring-cloud-starter-appconfiguration-config) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/README.md b/sdk/spring/spring-cloud-azure-appconfiguration-config/README.md index 9e2717e8dff0e..a4f3d6cbaba9e 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/README.md +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/README.md @@ -1,3 +1,3 @@ # Spring Cloud for Azure appconfiguration config client library for Java -See: [Spring Cloud for Azure App Configuration Starter](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/appconfiguration/azure-spring-cloud-starter-appconfiguration-config) \ No newline at end of file +See: [Spring Cloud for Azure App Configuration Starter](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/azure-spring-cloud-starter-appconfiguration-config) diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/README.md b/sdk/spring/spring-cloud-azure-feature-management-web/README.md index e5d2241ad891d..93242d4ad814a 100644 --- a/sdk/spring/spring-cloud-azure-feature-management-web/README.md +++ b/sdk/spring/spring-cloud-azure-feature-management-web/README.md @@ -1,3 +1,3 @@ # Spring Cloud for Azure feature management web client library for Java -See: [Spring Cloud Azure Feature Management](https://github.com/Azure/azure-sdk-for-java/blob/feature/azconfig-spring/DynamicFeature/sdk/appconfiguration/spring-cloud-azure-feature-management) +See: [Spring Cloud Azure Feature Management](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/azure-spring-cloud-feature-management) From 37ecab2a77f273507f7d7a44e6f4e3214a8f560d Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 6 Feb 2023 16:57:20 -0800 Subject: [PATCH 07/38] Updated Test Project --- .../spring/cloud/config/AppConfiguration.java | 4 ++-- .../spring/cloud/config/CustomClient.java | 20 +++++++++++++++++ .../spring/cloud/config/MyCredentials.java | 22 ------------------- 3 files changed, 22 insertions(+), 24 deletions(-) create mode 100644 sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java delete mode 100644 sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MyCredentials.java diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java index 7960c82f0c978..be90ab8f9400f 100644 --- a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java +++ b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java @@ -7,7 +7,7 @@ public class AppConfiguration { @Bean - public MyCredentials azureCredentials() { - return new MyCredentials(); + public CustomClient azureCredentials() { + return new CustomClient(); } } \ No newline at end of file diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java new file mode 100644 index 0000000000000..3ccd1d0954d34 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java @@ -0,0 +1,20 @@ +package com.azure.spring.cloud.config; + +import com.azure.core.credential.TokenCredential; +import com.azure.data.appconfiguration.ConfigurationClientBuilder; +import com.azure.identity.EnvironmentCredentialBuilder; + +public class CustomClient implements ConfigurationClientCustomizer { + + + + TokenCredential buildCredential() { + return new EnvironmentCredentialBuilder().build(); + } + + @Override + public void setup(ConfigurationClientBuilder builder, String endpoint) { + builder.credential(buildCredential()); + } + +} \ No newline at end of file diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MyCredentials.java b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MyCredentials.java deleted file mode 100644 index dffdbdef437e6..0000000000000 --- a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MyCredentials.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.azure.spring.cloud.config; - -import com.azure.core.credential.TokenCredential; -import com.azure.identity.EnvironmentCredentialBuilder; - -public class MyCredentials implements AppConfigurationCredentialProvider, KeyVaultCredentialProvider { - - @Override - public TokenCredential getKeyVaultCredential(String uri) { - return buildCredential(); - } - - @Override - public TokenCredential getAppConfigCredential(String uri) { - return buildCredential(); - } - - TokenCredential buildCredential() { - return new EnvironmentCredentialBuilder().build(); - } - -} \ No newline at end of file From 3da321c638be6113824b25998d003ef78cd10730 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 6 Feb 2023 17:04:54 -0800 Subject: [PATCH 08/38] Fixing names --- .../spring-cloud-azure-appconfiguration-config-web/README.md | 2 +- sdk/spring/spring-cloud-azure-appconfiguration-config/README.md | 2 +- sdk/spring/spring-cloud-azure-feature-management-web/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/README.md b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/README.md index cb22d8358da31..5cc8976aba684 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/README.md +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/README.md @@ -1,3 +1,3 @@ # Spring Cloud for Azure appconfiguration config web client library for Java -See: [Spring Cloud for Azure App Configuration Starter](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/azure-spring-cloud-starter-appconfiguration-config) +See: [Spring Cloud for Azure App Configuration Starter](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/spring-cloud-azure-starter-appconfiguration-config) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/README.md b/sdk/spring/spring-cloud-azure-appconfiguration-config/README.md index a4f3d6cbaba9e..e80c622e8bf04 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/README.md +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/README.md @@ -1,3 +1,3 @@ # Spring Cloud for Azure appconfiguration config client library for Java -See: [Spring Cloud for Azure App Configuration Starter](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/azure-spring-cloud-starter-appconfiguration-config) +See: [Spring Cloud for Azure App Configuration Starter](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/spring-cloud-azure-starter-appconfiguration-config) diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/README.md b/sdk/spring/spring-cloud-azure-feature-management-web/README.md index 93242d4ad814a..6d242c5237ebc 100644 --- a/sdk/spring/spring-cloud-azure-feature-management-web/README.md +++ b/sdk/spring/spring-cloud-azure-feature-management-web/README.md @@ -1,3 +1,3 @@ # Spring Cloud for Azure feature management web client library for Java -See: [Spring Cloud Azure Feature Management](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/azure-spring-cloud-feature-management) +See: [Spring Cloud Azure Feature Management](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/spring-cloud-azure-feature-management) From 2f4e17aa4143f15affbed8cc5286f60fdfe8744a Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 7 Feb 2023 14:44:35 -0800 Subject: [PATCH 09/38] Code Review Comments + Fix to Feature FIlters --- .../pom.xml | 6 ------ .../pom.xml | 16 ---------------- .../FeatureManagementProperties.java | 4 +++- 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml index e156496d4ef1a..cb299f172ef85 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml @@ -43,12 +43,6 @@ 3.1.2 true - - org.mockito - mockito-core - 4.5.1 - test - org.springframework.boot spring-boot-starter-test diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index 43e5ea247a0e6..e924b0798f303 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -31,12 +31,6 @@ spring-boot-autoconfigure 2.7.8 - - org.springframework.boot - spring-boot-configuration-processor - 2.7.8 - true - org.springframework.cloud spring-cloud-starter-bootstrap @@ -78,21 +72,11 @@ azure-security-keyvault-secrets 4.5.3 - - com.azure - azure-core-http-netty - 1.13.0 - org.hibernate.validator hibernate-validator 6.2.5.Final - - org.springframework.boot - spring-boot-actuator-autoconfigure - 2.7.8 - com.azure.spring spring-cloud-azure-service diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementProperties.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementProperties.java index 17d8dcf510edd..0a28ef4b37fb8 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementProperties.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementProperties.java @@ -12,6 +12,7 @@ import com.azure.spring.cloud.feature.manager.implementation.models.Feature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; /** * Configuration Properties for Feature Management. Processes the configurations to be usable by Feature Management. @@ -21,7 +22,8 @@ public class FeatureManagementProperties extends HashMap { private static final Logger LOGGER = LoggerFactory.getLogger(FeatureManagementProperties.class); - private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final ObjectMapper MAPPER = new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE); private static final long serialVersionUID = -1642032123104805346L; From 07cf8765e237adfb1f30972d2ab72fe6345d2875 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 7 Feb 2023 16:28:42 -0800 Subject: [PATCH 10/38] Removing Extra dependency --- .../spring-cloud-azure-appconfiguration-config/pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index e924b0798f303..90ce8ddb269a4 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -21,11 +21,6 @@ - - org.springframework.boot - spring-boot-autoconfigure-processor - 2.7.8 - org.springframework.boot spring-boot-autoconfigure From 563a7915b6c257c53734aa116a93dcce01535696 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 8 Feb 2023 10:49:16 -0800 Subject: [PATCH 11/38] Fixing Dependencies --- .../spring-cloud-azure-appconfiguration-config/pom.xml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index 90ce8ddb269a4..c1aa39b1002e5 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -75,12 +75,12 @@ com.azure.spring spring-cloud-azure-service - 4.5.0 + 4.6.0 com.azure.spring spring-cloud-azure-actuator-autoconfigure - 4.5.0 + 4.6.0 @@ -122,16 +122,10 @@ com.fasterxml.jackson.core:jackson-annotations:[2.13.4] com.fasterxml.jackson.core:jackson-databind:[2.13.4.2] javax.annotation:javax.annotation-api:[1.3.2] - org.apache.commons:commons-lang3:[3.12.0] - org.apache.httpcomponents:httpclient:[4.5.14] org.hibernate.validator:hibernate-validator:[6.2.5.Final] - org.springframework.boot:spring-boot-autoconfigure-processor:[2.7.8] org.springframework.boot:spring-boot-autoconfigure:[2.7.8] - org.springframework.boot:spring-boot-actuator-autoconfigure:[2.7.8] - org.springframework.boot:spring-boot-configuration-processor:[2.7.8] org.springframework.cloud:spring-cloud-context:[3.1.5] org.springframework.cloud:spring-cloud-starter-bootstrap:[3.1.5] - org.springframework:spring-web:[5.3.25] From 3005fa03e1c034042226daf18c778220f8779acc Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 8 Feb 2023 11:22:20 -0800 Subject: [PATCH 12/38] Trying to fix pom files --- .../pom.xml | 39 +++++++------------ .../pom.xml | 5 --- 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-feature-management/pom.xml b/sdk/spring/spring-cloud-azure-feature-management/pom.xml index c457d185bbcd7..20d5e7a08b2dc 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/pom.xml +++ b/sdk/spring/spring-cloud-azure-feature-management/pom.xml @@ -11,8 +11,7 @@ com.azure.spring spring-cloud-azure-feature-management - 4.0.0-beta.3 + 4.0.0-beta.3 Spring Cloud Azure Feature Management Adds Feature Management into Spring @@ -33,38 +32,32 @@ org.springframework spring-context - 5.3.25 + 5.3.25 org.springframework.boot spring-boot-starter - 2.7.8 + 2.7.8 com.fasterxml.jackson.core jackson-annotations - 2.13.4 + 2.13.4 com.fasterxml.jackson.core jackson-databind - 2.13.4.2 + 2.13.4.2 io.projectreactor.netty reactor-netty - 1.0.27 + 1.0.27 org.springframework.boot spring-boot-starter-test - 2.7.8 + 2.7.8 test @@ -73,22 +66,16 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.0.0-M3 + 3.0.0-M3 - com.fasterxml.jackson.core:jackson-annotations:[2.13.4] - com.fasterxml.jackson.core:jackson-databind:[2.13.4.2] - io.projectreactor.netty:reactor-netty:[1.0.27] - org.springframework.boot:spring-boot-starter:[2.7.8] - org.springframework:spring-context:[5.3.25] + com.fasterxml.jackson.core:jackson-annotations:[2.13.4] + com.fasterxml.jackson.core:jackson-databind:[2.13.4.2] + io.projectreactor.netty:reactor-netty:[1.0.27] + org.springframework.boot:spring-boot-starter:[2.7.8] + org.springframework:spring-context:[5.3.25] diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml index 9db673aaf1aa9..dcdd4bb9a0a23 100644 --- a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml @@ -26,11 +26,6 @@ spring-cloud-azure-starter-appconfiguration-config 4.0.0-beta.1 - - com.microsoft.azure - azure - 1.34.0 - org.springframework.boot spring-boot-starter-web From bcf34f1585becc1dec70f0bb37a34268b85050a3 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 9 Feb 2023 14:47:42 -0800 Subject: [PATCH 13/38] Small code review changes. Updated Java Docs, changed a few names. Removed old dependencies. --- .../web/AppConfigurationWebAutoConfiguration.java | 3 --- .../pom.xml | 10 ---------- .../cloud/config/ConfigurationClientCustomizer.java | 2 +- .../spring/cloud/config/SecretClientCustomizer.java | 2 +- .../AppConfigurationReplicaClientsBuilder.java | 2 +- .../config/AppConfigurationAutoConfiguration.java | 4 ++-- .../AppConfigurationBootstrapConfiguration.java | 13 ------------- .../stores/AppConfigurationSecretClientManager.java | 6 +++--- .../AppConfigurationReplicaClientBuilderTest.java | 2 +- .../FeatureManagementWebConfiguration.java | 6 +++--- .../manager/implementation/package-info.java | 3 --- .../manager/implementation/models/package-info.java | 6 ------ .../manager/implementation/package-info.java | 8 -------- 13 files changed, 12 insertions(+), 55 deletions(-) delete mode 100644 sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/package-info.java delete mode 100644 sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java index 5f58b3777f822..1f8bd1982940e 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java @@ -43,9 +43,6 @@ AppConfigurationEventListener configListener(AppConfigurationRefresh appConfigur return new AppConfigurationEventListener(appConfigurationRefresh); } - /** - * Refresh from Pull Requests - */ @Configuration @ConditionalOnClass(name = { "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties", diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index c1aa39b1002e5..f61256e59172f 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -36,16 +36,6 @@ spring-cloud-context 3.1.5 - - com.fasterxml.jackson.core - jackson-annotations - 2.13.4 - - - com.fasterxml.jackson.core - jackson-databind - 2.13.4.2 - com.azure diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/ConfigurationClientCustomizer.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/ConfigurationClientCustomizer.java index 50ef2137808b8..e3b7855cb99b2 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/ConfigurationClientCustomizer.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/ConfigurationClientCustomizer.java @@ -15,6 +15,6 @@ public interface ConfigurationClientCustomizer { * @param builder ConfigurationClientBuilder * @param endpoint String */ - void setup(ConfigurationClientBuilder builder, String endpoint); + void customize(ConfigurationClientBuilder builder, String endpoint); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/SecretClientCustomizer.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/SecretClientCustomizer.java index 7c749b5f9df6c..db0f3406a549f 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/SecretClientCustomizer.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/SecretClientCustomizer.java @@ -14,6 +14,6 @@ public interface SecretClientCustomizer { * @param builder SecretClientBuilder * @param endpoint String */ - void setup(SecretClientBuilder builder, String endpoint); + void customize(SecretClientBuilder builder, String endpoint); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java index 8b788a85d3287..95c5535e32626 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java @@ -185,7 +185,7 @@ private AppConfigurationReplicaClient modifyAndBuildClient(ConfigurationClientBu builder.addPolicy(new BaseAppConfigurationPolicy(isDev, isKeyVaultConfigured, replicaCount)); if (clientProvider != null) { - clientProvider.setup(builder, endpoint); + clientProvider.customize(builder, endpoint); } return new AppConfigurationReplicaClient(endpoint, builder.buildClient()); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationAutoConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationAutoConfiguration.java index d2792e1195b23..a5f3468f813f2 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationAutoConfiguration.java @@ -29,11 +29,11 @@ public class AppConfigurationAutoConfiguration { */ @Configuration @ConditionalOnClass(RefreshEndpoint.class) - public static class AppConfigurationWatchAutoConfiguration { + static class AppConfigurationWatchAutoConfiguration { @Bean @ConditionalOnMissingBean - public AppConfigurationRefresh appConfigurationRefresh(AppConfigurationProperties properties, + AppConfigurationRefresh appConfigurationRefresh(AppConfigurationProperties properties, AppConfigurationProviderProperties appProperties, AppConfigurationReplicaClientFactory clientFactory) { return new AppConfigurationPullRefresh(clientFactory, properties.getRefreshInterval(), appProperties.getDefaultMinBackoff()); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java index d405fb2229cd6..3d945bc6724a7 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java @@ -50,15 +50,6 @@ public class AppConfigurationBootstrapConfiguration { @Autowired private transient ApplicationContext context; - /** - * - * @param properties Client properties - * @param appProperties Library properties - * @param clientFactory Store Connections - * @param keyVaultClientFactory keyVaultClientFactory - * @return AppConfigurationPropertySourceLocator - * @throws IllegalArgumentException if both KeyVaultClientProvider and KeyVaultSecretProvider exist. - */ @Bean AppConfigurationPropertySourceLocator sourceLocator(AppConfigurationProperties properties, AppConfigurationProviderProperties appProperties, AppConfigurationReplicaClientFactory clientFactory, @@ -69,10 +60,6 @@ AppConfigurationPropertySourceLocator sourceLocator(AppConfigurationProperties p properties.getRefreshInterval(), properties.getStores()); } - /** - * @param clientProperties AzureKeyVaultSecretProvider client properties - * @throws IllegalArgumentException if both KeyVaultClientProvider and KeyVaultSecretProvider exist. - */ @Bean AppConfigurationKeyVaultClientFactory keyVaultClientFactory(Environment environment) throws IllegalArgumentException { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/stores/AppConfigurationSecretClientManager.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/stores/AppConfigurationSecretClientManager.java index c0d9c7927a2c9..e495280def37e 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/stores/AppConfigurationSecretClientManager.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/stores/AppConfigurationSecretClientManager.java @@ -35,10 +35,10 @@ public final class AppConfigurationSecretClientManager { /** * Creates a Client for connecting to Key Vault * @param endpoint Key Vault endpoint - * @param tokenCredentialProvider optional provider of the Token Credential for connecting to Key Vault * @param keyVaultClientProvider optional provider for overriding the Key Vault Client * @param keyVaultSecretProvider optional provider for providing Secrets instead of connecting to Key Vault - * @param authClientId clientId used to authenticate with to App Configuration (Optional) + * @param secretClientFactory Factory for building clients to Key Vault + * @param credentialConfigured Is a credential configured with Global Configurations or Service Configurations */ public AppConfigurationSecretClientManager(String endpoint, SecretClientCustomizer keyVaultClientProvider, KeyVaultSecretProvider keyVaultSecretProvider, SecretClientBuilderFactory secretClientFactory, boolean credentialConfigured) { @@ -59,7 +59,7 @@ AppConfigurationSecretClientManager build() { builder.vaultUrl(endpoint); if (keyVaultClientProvider != null) { - keyVaultClientProvider.setup(builder, endpoint); + keyVaultClientProvider.customize(builder, endpoint); } secretClient = builder.buildAsyncClient(); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientBuilderTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientBuilderTest.java index 66c6ba452d793..b0bc41f3c63f1 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientBuilderTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientBuilderTest.java @@ -126,7 +126,7 @@ public void modifyClientTest() { assertEquals(TEST_ENDPOINT, replicaClient.getEndpoint()); assertEquals(0, replicaClient.getFailedAttempts()); - verify(modifierMock, times(1)).setup(Mockito.eq(builderMock), Mockito.eq(TEST_ENDPOINT)); + verify(modifierMock, times(1)).customize(Mockito.eq(builderMock), Mockito.eq(TEST_ENDPOINT)); } @Test diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementWebConfiguration.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementWebConfiguration.java index 4ae1f5301095a..d9e56afae0008 100644 --- a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementWebConfiguration.java +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/FeatureManagementWebConfiguration.java @@ -30,7 +30,7 @@ public class FeatureManagementWebConfiguration { */ @Bean @RequestScope - public FeatureManagerSnapshot featureManagerSnapshot(FeatureManager featureManager) { + FeatureManagerSnapshot featureManagerSnapshot(FeatureManager featureManager) { return new FeatureManagerSnapshot(featureManager); } @@ -43,7 +43,7 @@ public FeatureManagerSnapshot featureManagerSnapshot(FeatureManager featureManag * @return FeatureHandler */ @Bean - public FeatureHandler featureHandler(FeatureManager featureManager, FeatureManagerSnapshot snapshot, + FeatureHandler featureHandler(FeatureManager featureManager, FeatureManagerSnapshot snapshot, @Autowired(required = false) IDisabledFeaturesHandler disabledFeaturesHandler) { return new FeatureHandler(featureManager, snapshot, disabledFeaturesHandler); } @@ -55,7 +55,7 @@ public FeatureHandler featureHandler(FeatureManager featureManager, FeatureManag * @return FeatureConfig */ @Bean - public FeatureConfig featureConfig(FeatureHandler featureHandler) { + FeatureConfig featureConfig(FeatureHandler featureHandler) { return new FeatureConfig(featureHandler); } diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java deleted file mode 100644 index 98727121d121b..0000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java +++ /dev/null @@ -1,3 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.spring.cloud.feature.manager.implementation; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/package-info.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/package-info.java deleted file mode 100644 index 885a91e581dab..0000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/models/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package containing implementation models classes for setting up Feature Flags, Feature Filters, and Feature Variants. - */ -package com.azure.spring.cloud.feature.manager.implementation.models; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java deleted file mode 100644 index 59c47a44659f8..0000000000000 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/implementation/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package containing implementation classes for initializing - * {@link com.azure.spring.cloud.feature.manager.FeatureManager} and - * {@link com.azure.spring.cloud.feature.manager.DynamicFeatureManager}. - */ -package com.azure.spring.cloud.feature.manager.implementation; From 51b0d77b566cd31dabbf83d69be0bcdab30d69aa Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 9 Feb 2023 14:57:18 -0800 Subject: [PATCH 14/38] updating AppConfigurationKeyVaultClientFactory bean name --- .../config/AppConfigurationBootstrapConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java index 3d945bc6724a7..dbd41fdd8a2d9 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java @@ -61,7 +61,7 @@ AppConfigurationPropertySourceLocator sourceLocator(AppConfigurationProperties p } @Bean - AppConfigurationKeyVaultClientFactory keyVaultClientFactory(Environment environment) + AppConfigurationKeyVaultClientFactory appConfigurationKeyVaultClientFactory(Environment environment) throws IllegalArgumentException { AzureGlobalProperties globalSource = Binder.get(environment).bindOrCreate(AzureGlobalProperties.PREFIX, AzureGlobalProperties.class); From a5d1576b1bdcea7abfeefed9fde7149ee13a3ab3 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 9 Feb 2023 15:51:45 -0800 Subject: [PATCH 15/38] Fixing Provider Properties --- .../AppConfigurationBootstrapConfiguration.java | 17 +++++++++-------- .../properties/AppConfigurationProperties.java | 2 -- .../AppConfigurationProviderProperties.java | 4 +--- .../main/resources/appConfiguration.properties | 6 ++++++ .../src/main/resources/appConfiguration.yaml | 9 --------- 5 files changed, 16 insertions(+), 22 deletions(-) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/appConfiguration.properties delete mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/appConfiguration.yaml diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java index dbd41fdd8a2d9..5e1c2fa9af05a 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/config/AppConfigurationBootstrapConfiguration.java @@ -12,6 +12,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; import org.springframework.util.StringUtils; @@ -42,6 +43,7 @@ * spring.cloud.azure.appconfiguration.enabled is enabled. */ @Configuration +@PropertySource("classpath:appConfiguration.properties") @EnableConfigurationProperties({ AppConfigurationProperties.class, AppConfigurationProviderProperties.class }) @ConditionalOnClass(AppConfigurationPropertySourceLocator.class) @ConditionalOnProperty(prefix = AppConfigurationProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true) @@ -82,10 +84,11 @@ AppConfigurationKeyVaultClientFactory appConfigurationKeyVaultClientFactory(Envi .getIfAvailable(); SecretClientBuilderFactory secretClientBuilderFactory = new SecretClientBuilderFactory(clientProperties); - + boolean credentialConfigured = isCredentialConfigured(clientProperties); - return new AppConfigurationKeyVaultClientFactory(keyVaultClientProvider, keyVaultSecretProvider, secretClientBuilderFactory, credentialConfigured); + return new AppConfigurationKeyVaultClientFactory(keyVaultClientProvider, keyVaultSecretProvider, + secretClientBuilderFactory, credentialConfigured); } /** @@ -133,12 +136,12 @@ AppConfigurationReplicaClientsBuilder replicaClientBuilder(Environment environme clientFactory.setSpringIdentifier(AzureSpringIdentifier.AZURE_SPRING_APP_CONFIG); customizers.orderedStream().forEach(clientFactory::addBuilderCustomizer); - + boolean credentialConfigured = isCredentialConfigured(clientProperties); AppConfigurationReplicaClientsBuilder clientBuilder = new AppConfigurationReplicaClientsBuilder( appProperties.getMaxRetries(), clientFactory, credentialConfigured); - + clientBuilder .setClientProvider(context.getBeanProvider(ConfigurationClientCustomizer.class) .getIfAvailable()); @@ -147,7 +150,7 @@ AppConfigurationReplicaClientsBuilder replicaClientBuilder(Environment environme return clientBuilder; } - + private boolean isCredentialConfigured(AbstractAzureHttpConfigurationProperties properties) { if (properties.getCredential() != null) { TokenCredentialConfigurationProperties tokenProps = properties.getCredential(); @@ -165,10 +168,8 @@ private boolean isCredentialConfigured(AbstractAzureHttpConfigurationProperties return true; } } - + return false; } - - } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProperties.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProperties.java index 2c1829556f797..d95133be6ec8b 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProperties.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProperties.java @@ -11,7 +11,6 @@ import javax.annotation.PostConstruct; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Import; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -22,7 +21,6 @@ */ @Validated @ConfigurationProperties(prefix = AppConfigurationProperties.CONFIG_PREFIX) -@Import({ AppConfigurationProviderProperties.class }) public final class AppConfigurationProperties { /** diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProviderProperties.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProviderProperties.java index 5a54562d03d55..2331436e753d0 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProviderProperties.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProviderProperties.java @@ -9,16 +9,14 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.validation.annotation.Validated; /** * Properties defining connection to Azure App Configuration. */ -@Configuration @Validated -@PropertySource("classpath:appConfiguration.yaml") +@PropertySource("classpath:appConfiguration.properties") @ConfigurationProperties(prefix = AppConfigurationProviderProperties.CONFIG_PREFIX) public class AppConfigurationProviderProperties { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/appConfiguration.properties b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/appConfiguration.properties new file mode 100644 index 0000000000000..9b90ce63ee85b --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/appConfiguration.properties @@ -0,0 +1,6 @@ +spring.cloud.appconfiguration.version=1.0 +spring.cloud.appconfiguration.maxRetries=2 +spring.cloud.appconfiguration.maxRetryTime=60 +spring.cloud.appconfiguration.preKillTime=5 +spring.cloud.appconfiguration.defaultMinBackoff=30 +spring.cloud.appconfiguration.defaultmaxBackoff=600 diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/appConfiguration.yaml b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/appConfiguration.yaml deleted file mode 100644 index 9654fca24e4e5..0000000000000 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/appConfiguration.yaml +++ /dev/null @@ -1,9 +0,0 @@ -spring: - cloud: - appconfiguration: - version: 1.0 - maxRetries: 2 - maxRetryTime: 60 - preKillTime: 5 - defaultMinBackoff: 30 - defaultmaxBackoff: 600 From 910a38b117db036754c4eab005a6c9fe5df331c6 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 9 Feb 2023 15:55:34 -0800 Subject: [PATCH 16/38] Removing extra annotation --- .../properties/AppConfigurationProviderProperties.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProviderProperties.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProviderProperties.java index 2331436e753d0..26b738ee2934f 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProviderProperties.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProviderProperties.java @@ -9,14 +9,12 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.PropertySource; import org.springframework.validation.annotation.Validated; /** * Properties defining connection to Azure App Configuration. */ @Validated -@PropertySource("classpath:appConfiguration.properties") @ConfigurationProperties(prefix = AppConfigurationProviderProperties.CONFIG_PREFIX) public class AppConfigurationProviderProperties { From fc2b4058479ff09425f97c22d0467cde18747c40 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 9 Feb 2023 16:49:01 -0800 Subject: [PATCH 17/38] Fixing Property validation --- .../pom.xml | 254 +++++++++--------- .../AppConfigurationProperties.java | 2 - .../AppConfigurationProviderProperties.java | 22 +- 3 files changed, 133 insertions(+), 145 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index f61256e59172f..3e915c92ea1ed 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -1,139 +1,127 @@ - - com.azure - azure-client-sdk-parent - 1.7.0 - ../../parents/azure-client-sdk-parent - - 4.0.0 - - com.azure.spring - spring-cloud-azure-appconfiguration-config - 4.0.0-beta.1 - Azure Spring Cloud App Configuration Config - Integration of Spring Cloud Config and Azure App Configuration Service - - - false - - - - - - - org.springframework.boot - spring-boot-autoconfigure - 2.7.8 - - - org.springframework.cloud - spring-cloud-starter-bootstrap - 3.1.5 - - - org.springframework.cloud - spring-cloud-context - 3.1.5 - - - - com.azure - azure-core - 1.36.0 - - - com.azure - azure-data-appconfiguration - 1.4.1 - - - com.azure - azure-identity - 1.8.0 - - - com.azure - azure-security-keyvault-secrets - 4.5.3 - - - org.hibernate.validator - hibernate-validator - 6.2.5.Final - - - com.azure.spring - spring-cloud-azure-service - 4.6.0 - - - com.azure.spring - spring-cloud-azure-actuator-autoconfigure - 4.6.0 - - - - - org.springframework.boot - spring-boot-starter-test - 2.7.8 - test - - - + ../../parents/azure-client-sdk-parent + + 4.0.0 + com.azure.spring + spring-cloud-azure-appconfiguration-config + 4.0.0-beta.1 + Azure Spring Cloud App Configuration Config + Integration of Spring Cloud Config and Azure App Configuration Service + + false + + + + + + org.springframework.boot + spring-boot-autoconfigure + 2.7.8 + + + org.springframework.cloud + spring-cloud-starter-bootstrap + 3.1.5 + + + org.springframework.cloud + spring-cloud-context + 3.1.5 + + + + com.azure + azure-core + 1.36.0 + + + com.azure + azure-data-appconfiguration + 1.4.1 + + + com.azure + azure-identity + 1.8.0 + + + com.azure + azure-security-keyvault-secrets + 4.5.3 + + + com.azure.spring + spring-cloud-azure-service + 4.6.0 + + + com.azure.spring + spring-cloud-azure-actuator-autoconfigure + 4.6.0 + + + + org.springframework.boot + spring-boot-starter-test + 2.7.8 + test + + - - com.google.code.findbugs - jsr305 - 3.0.2 - provided - - - javax.annotation - javax.annotation-api - 1.3.2 - - - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.0.0-M3 - - - - - com.fasterxml.jackson.core:jackson-annotations:[2.13.4] - com.fasterxml.jackson.core:jackson-databind:[2.13.4.2] - javax.annotation:javax.annotation-api:[1.3.2] - org.hibernate.validator:hibernate-validator:[6.2.5.Final] - org.springframework.boot:spring-boot-autoconfigure:[2.7.8] - org.springframework.cloud:spring-cloud-context:[3.1.5] - org.springframework.cloud:spring-cloud-starter-bootstrap:[3.1.5] - - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.1.2 - - - - true - true - - - - - - + + com.google.code.findbugs + jsr305 + 3.0.2 + provided + + + javax.annotation + javax.annotation-api + 1.3.2 + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.fasterxml.jackson.core:jackson-annotations:[2.13.4] + com.fasterxml.jackson.core:jackson-databind:[2.13.4.2] + javax.annotation:javax.annotation-api:[1.3.2] + org.hibernate.validator:hibernate-validator:[6.2.5.Final] + org.springframework.boot:spring-boot-autoconfigure:[2.7.8] + org.springframework.cloud:spring-cloud-context:[3.1.5] + org.springframework.cloud:spring-cloud-starter-bootstrap:[3.1.5] + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.2 + + + + true + true + + + + + + diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProperties.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProperties.java index d95133be6ec8b..eb512f9d0bd95 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProperties.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProperties.java @@ -14,12 +14,10 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import org.springframework.validation.annotation.Validated; /** * Properties for all Azure App Configuration stores that are loaded. */ -@Validated @ConfigurationProperties(prefix = AppConfigurationProperties.CONFIG_PREFIX) public final class AppConfigurationProperties { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProviderProperties.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProviderProperties.java index 26b738ee2934f..efb3e85062174 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProviderProperties.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/properties/AppConfigurationProviderProperties.java @@ -4,17 +4,15 @@ import java.time.Instant; -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; +import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; +import org.springframework.util.Assert; /** * Properties defining connection to Azure App Configuration. */ -@Validated @ConfigurationProperties(prefix = AppConfigurationProviderProperties.CONFIG_PREFIX) public class AppConfigurationProviderProperties { @@ -25,27 +23,21 @@ public class AppConfigurationProviderProperties { private static final Instant START_DATE = Instant.now(); - @NotEmpty @Value("${version:1.0}") private String version; - @NotNull @Value("${maxRetries:2}") private int maxRetries; - @NotNull @Value("${maxRetryTime:60}") private int maxRetryTime; - @NotNull @Value("${prekillTime:5}") private int prekillTime; - @NotNull @Value("${defaultMinBackoff:30}") private Long defaultMinBackoff; - @NotNull @Value("${defaultMaxBackoff:600}") private Long defaultMaxBackoff; @@ -139,5 +131,15 @@ public Long getDefaultMaxBackoff() { public void setDefaultMaxBackoff(Long defaultMaxBackoff) { this.defaultMaxBackoff = defaultMaxBackoff; } + + @PostConstruct + public void validateAndInit() { + Assert.hasLength(version, "A version of app configuration should be set."); + Assert.notNull(maxRetries, "A number of max retries has to be configured."); + Assert.notNull(maxRetryTime, "A max retry value needs to be configured"); + Assert.notNull(prekillTime, "A preKill time value needs to be configured."); + Assert.notNull(defaultMinBackoff, "A default minimum backoff time value needs to be set."); + Assert.notNull(defaultMaxBackoff, "A default max backoff time value needs to be set."); + } } From 57388813eb186ec05937587d2a51e3114d1c82ac Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 9 Feb 2023 18:24:33 -0800 Subject: [PATCH 18/38] Fixing integration tests --- .../test/java/com/azure/spring/cloud/config/CustomClient.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java index 3ccd1d0954d34..22e8a5410c30b 100644 --- a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java +++ b/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java @@ -6,14 +6,12 @@ public class CustomClient implements ConfigurationClientCustomizer { - - TokenCredential buildCredential() { return new EnvironmentCredentialBuilder().build(); } @Override - public void setup(ConfigurationClientBuilder builder, String endpoint) { + public void customize(ConfigurationClientBuilder builder, String endpoint) { builder.credential(buildCredential()); } From 4c700f32eb898c5b42e880efbef740075aab2b46 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 10 Feb 2023 11:16:59 -0800 Subject: [PATCH 19/38] Cleaning up unused @Component s --- .../pullrefresh/AppConfigurationEventListener.java | 2 -- .../pushbusrefresh/AppConfigurationBusRefreshEventListener.java | 2 -- .../pushrefresh/AppConfigurationRefreshEventListener.java | 2 -- 3 files changed, 6 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pullrefresh/AppConfigurationEventListener.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pullrefresh/AppConfigurationEventListener.java index db6d0a3c830fd..1972b53e0d61a 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pullrefresh/AppConfigurationEventListener.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pullrefresh/AppConfigurationEventListener.java @@ -9,7 +9,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationListener; -import org.springframework.stereotype.Component; import org.springframework.web.context.support.ServletRequestHandledEvent; import com.azure.spring.cloud.config.AppConfigurationRefresh; @@ -17,7 +16,6 @@ /** * Listens for ServletRequestHandledEvents to check if the configurations need to be updated. */ -@Component public final class AppConfigurationEventListener implements ApplicationListener { private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationEventListener.class); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEventListener.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEventListener.java index cdff1080fe93f..d37b80b2635c6 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEventListener.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEventListener.java @@ -5,14 +5,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationListener; -import org.springframework.stereotype.Component; import com.azure.spring.cloud.config.AppConfigurationRefresh; /** * Listens for AppConfigurationBusRefreshEvents and sets the App Configuration watch interval to zero. */ -@Component public final class AppConfigurationBusRefreshEventListener implements ApplicationListener { private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationBusRefreshEventListener.class); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEventListener.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEventListener.java index 0b8d3df4f2d06..095e571d25f16 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEventListener.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/implementation/pushrefresh/AppConfigurationRefreshEventListener.java @@ -5,14 +5,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationListener; -import org.springframework.stereotype.Component; import com.azure.spring.cloud.config.AppConfigurationRefresh; /** * Listens for AppConfigurationRefreshEvents and sets the App Configuration watch interval to zero. */ -@Component public final class AppConfigurationRefreshEventListener implements ApplicationListener { private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationRefreshEventListener.class); From 4d2e66b1dba320971c16bf0fe08c7c7b67dad314 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 10 Feb 2023 11:32:52 -0800 Subject: [PATCH 20/38] Removing Feature Flag Components --- .../cloud/feature/manager/feature/filters/PercentageFilter.java | 2 -- .../cloud/feature/manager/feature/filters/TimeWindowFilter.java | 2 -- 2 files changed, 4 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilter.java index 38030a253633d..e063eb2cb0622 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/PercentageFilter.java @@ -6,7 +6,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; import com.azure.spring.cloud.feature.manager.models.IFeatureFilter; @@ -14,7 +13,6 @@ /** * A feature filter that can be used to activate a feature based on a random percentage. */ -@Component("PercentageFilter") public final class PercentageFilter implements IFeatureFilter { private static final Logger LOGGER = LoggerFactory.getLogger(PercentageFilter.class); diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilter.java index b749d71723981..151b7638f16e6 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/feature/filters/TimeWindowFilter.java @@ -11,7 +11,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import com.azure.spring.cloud.feature.manager.models.FeatureFilterEvaluationContext; @@ -20,7 +19,6 @@ /** * A feature filter that can be used at activate a feature based on a time window. */ -@Component("TimeWindowFilter") public final class TimeWindowFilter implements IFeatureFilter { private static final Logger LOGGER = LoggerFactory.getLogger(TimeWindowFilter.class); From 5dcbfc0cab335d3f4405244194d800e4366053e5 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 10 Feb 2023 12:28:50 -0800 Subject: [PATCH 21/38] Updating Feature Handler --- .../spring/cloud/feature/manager/FeatureHandler.java | 10 +++++----- .../spring-cloud-azure-feature-management/README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureHandler.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureHandler.java index 5ea69e808846f..9d0eaa0ed60c2 100644 --- a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureHandler.java +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/FeatureHandler.java @@ -11,18 +11,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; import org.springframework.util.ReflectionUtils; import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +import org.springframework.web.servlet.HandlerInterceptor; import reactor.core.publisher.Mono; /** * Interceptor for Requests to check if they should be run. */ -@Component -public class FeatureHandler extends HandlerInterceptorAdapter { +public class FeatureHandler implements HandlerInterceptor { private static final Logger LOGGER = LoggerFactory.getLogger(FeatureHandler.class); @@ -49,10 +47,12 @@ public FeatureHandler(FeatureManager featureManager, FeatureManagerSnapshot feat * Checks if the endpoint being called has the @FeatureOn annotation. Checks if the feature is on. Can redirect if * feature is off, or can return the disabled feature handler. * + * @param request current HTTP request + * @param response current HTTP response + * @param handler the handler (or {@link HandlerMethod}) that started asynchronous * @return true if the @FeatureOn annotation is on or the feature is enabled. Else, it returns false, or is * redirected. */ - @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { Method method = null; if (handler instanceof HandlerMethod) { diff --git a/sdk/spring/spring-cloud-azure-feature-management/README.md b/sdk/spring/spring-cloud-azure-feature-management/README.md index 6085c683a1080..d8020de013597 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/README.md +++ b/sdk/spring/spring-cloud-azure-feature-management/README.md @@ -119,7 +119,7 @@ Certain routes may expose application capabilities that are gated by features. T ```java @GetMapping("/featureT") -@FeatureGate(feature = "feature-t" fallback= "/oldEndpoint") +@FeatureGate(feature = "feature-t", fallback= "/oldEndpoint") @ResponseBody public String featureT() { ... From 2bd2072c4324b953b74bfc82317572ed0fb2041e Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 10 Feb 2023 12:43:12 -0800 Subject: [PATCH 22/38] Removing unneeded @Component --- .../spring/cloud/feature/manager/IDisabledFeaturesHandler.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/IDisabledFeaturesHandler.java b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/IDisabledFeaturesHandler.java index 492ac9fd866e5..e85d56ef3052a 100644 --- a/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/IDisabledFeaturesHandler.java +++ b/sdk/spring/spring-cloud-azure-feature-management-web/src/main/java/com/azure/spring/cloud/feature/manager/IDisabledFeaturesHandler.java @@ -4,13 +4,11 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.springframework.stereotype.Component; /** * Interface for Disabled Features Handler. The Feature Handler checks to see if this Component is implemented before * blocking an endpoint. If not implemented a 404 is returned. */ -@Component public interface IDisabledFeaturesHandler { /** From 69fdc5fcf5e6230ab408e5ccb6562051cf9cf8ee Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 10 Feb 2023 14:14:12 -0800 Subject: [PATCH 23/38] Fixing Pipeline --- ...mpatibility_insert_dependencymanagement.py | 21 +++++++++++++------ .../spring/cloud/config/web/package-info.java | 6 ------ 2 files changed, 15 insertions(+), 12 deletions(-) delete mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java diff --git a/sdk/spring/scripts/compatibility_insert_dependencymanagement.py b/sdk/spring/scripts/compatibility_insert_dependencymanagement.py index 4b9d27918af8a..9a80b1b5bcdf6 100644 --- a/sdk/spring/scripts/compatibility_insert_dependencymanagement.py +++ b/sdk/spring/scripts/compatibility_insert_dependencymanagement.py @@ -43,6 +43,7 @@ def add_dependency_management_for_all_poms_files_in_directory(directory, spring_ if file_name.startswith('pom') and file_name.endswith('.xml'): file_path = root + os.sep + file_name add_dependency_management_for_file(file_path, spring_boot_dependencies_version, spring_cloud_dependencies_version) + update_spring_boot_starter_parent_for_file(file_path, spring_boot_dependencies_version) def contains_repositories(pom_file_content): @@ -70,7 +71,7 @@ def get_repo_content(pom_file_content): if contains_repositories(pom_file_content): return get_repo_content_without_tag() else: - return """ + return """ {} @@ -101,7 +102,7 @@ def add_dependency_management_for_file(file_path, spring_boot_dependencies_versi with open(file_path, 'r', encoding = 'utf-8') as pom_file: pom_file_content = pom_file.read() insert_position = pom_file_content.find('') - if(insert_position == -1): + if insert_position == -1: # no dependencies section in pom, not adding section log.warn("No dependencies section found in " + file_path + ". Not adding dependencyManagement.") return @@ -121,6 +122,17 @@ def add_dependency_management_for_file(file_path, spring_boot_dependencies_versi with open(file_path, 'r+', encoding = 'utf-8') as updated_pom_file: updated_pom_file.writelines(repo_content) +def update_spring_boot_starter_parent_for_file(file_path, spring_boot_dependencies_version): + with open(file_path, 'r', encoding = 'utf-8') as pom_file: + pom_file_content = pom_file.read() + if pom_file_content.find('spring-boot-starter-parent') == -1: + log.debug("No spring-boot-starter-parent found in " + file_path + ". Not updating it.") + return + with open(file_path, 'r+', encoding = 'utf-8') as updated_pom_file: + log.info("Found spring-boot-starter-parent found in " + file_path + ". Now updating it.") + new_content = pom_file_content.replace('spring-boot-starter-parent', + 'spring-boot-starter-parent\n{}'.format(spring_boot_dependencies_version)) + updated_pom_file.writelines(new_content) def get_dependency_management_content(): return """ @@ -142,9 +154,8 @@ def get_dependency_management_content(): - -""" +""" def get_properties_contend_with_tag(spring_boot_dependencies_version, spring_cloud_dependencies_version): return """ @@ -154,13 +165,11 @@ def get_properties_contend_with_tag(spring_boot_dependencies_version, spring_clo """.format(get_properties_contend(spring_boot_dependencies_version, spring_cloud_dependencies_version)) - def get_properties_contend(spring_boot_dependencies_version, spring_cloud_dependencies_version): return """ {} {} """.format(spring_boot_dependencies_version, spring_cloud_dependencies_version) - if __name__ == '__main__': main() diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java deleted file mode 100644 index efdf0c7f84347..0000000000000 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -/** - * Package contains classes for enabling auto refresh of Azure App Configuration stores - */ -package com.azure.spring.cloud.config.web; From 6bc7c94de1e41d33e13a301a47a0b5e40607815e Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 10 Feb 2023 15:01:46 -0800 Subject: [PATCH 24/38] Few renames --- sdk/spring/pom.xml | 4 +-- .../AppConfigurationWebAutoConfiguration.java | 33 ------------------- .../AppConfigurationPullRefresh.java | 2 +- .../AppConfigurationRefreshUtil.java | 2 +- ...AppConfigurationReplicaClientsBuilder.java | 2 +- .../policy}/BaseAppConfigurationPolicy.java | 2 +- .../BaseAppConfigurationPolicyTest.java | 3 +- .../CHANGELOG.md | 0 .../README.md | 0 .../pom.xml | 4 +-- .../spring/cloud/config/AppConfiguration.java | 0 .../spring/cloud/config/CustomClient.java | 0 .../spring/cloud/config/LoadConfigsTest.java | 0 .../cloud/config/MessageProperties.java | 0 .../test/resources/META-INF/spring.factories | 0 .../test-resources.json | 0 sdk/spring/tests.yml | 11 ++++--- 17 files changed, 16 insertions(+), 47 deletions(-) rename sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/{pipline/policies => http/policy}/BaseAppConfigurationPolicy.java (98%) rename sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/{pipline/policies => http/policy}/BaseAppConfigurationPolicyTest.java (97%) rename sdk/spring/{spring-cloud-azure-test-appconfiguration-config => spring-cloud-azure-integration-test-appconfiguration-config}/CHANGELOG.md (100%) rename sdk/spring/{spring-cloud-azure-test-appconfiguration-config => spring-cloud-azure-integration-test-appconfiguration-config}/README.md (100%) rename sdk/spring/{spring-cloud-azure-test-appconfiguration-config => spring-cloud-azure-integration-test-appconfiguration-config}/pom.xml (94%) rename sdk/spring/{spring-cloud-azure-test-appconfiguration-config => spring-cloud-azure-integration-test-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java (100%) rename sdk/spring/{spring-cloud-azure-test-appconfiguration-config => spring-cloud-azure-integration-test-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/CustomClient.java (100%) rename sdk/spring/{spring-cloud-azure-test-appconfiguration-config => spring-cloud-azure-integration-test-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java (100%) rename sdk/spring/{spring-cloud-azure-test-appconfiguration-config => spring-cloud-azure-integration-test-appconfiguration-config}/src/test/java/com/azure/spring/cloud/config/MessageProperties.java (100%) rename sdk/spring/{spring-cloud-azure-test-appconfiguration-config => spring-cloud-azure-integration-test-appconfiguration-config}/src/test/resources/META-INF/spring.factories (100%) rename sdk/spring/{spring-cloud-azure-test-appconfiguration-config => spring-cloud-azure-integration-test-appconfiguration-config}/test-resources.json (100%) diff --git a/sdk/spring/pom.xml b/sdk/spring/pom.xml index ab2b5e96d0589..4541ce3359532 100644 --- a/sdk/spring/pom.xml +++ b/sdk/spring/pom.xml @@ -60,7 +60,7 @@ spring-cloud-azure-starter-jdbc-postgresql spring-cloud-azure-starter-redis spring-cloud-azure-integration-tests - spring-cloud-azure-test-appconfiguration-config + spring-cloud-azure-integration-test-appconfiguration-config spring-cloud-azure-appconfiguration-config spring-cloud-azure-appconfiguration-config-web spring-cloud-azure-feature-management @@ -118,7 +118,7 @@ spring-cloud-azure-starter-jdbc-mysql spring-cloud-azure-starter-jdbc-postgresql spring-cloud-azure-starter-redis - spring-cloud-azure-test-appconfiguration-config + spring-cloud-azure-integration-test-appconfiguration-config spring-cloud-azure-appconfiguration-config spring-cloud-azure-appconfiguration-config-web spring-cloud-azure-feature-management diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java index 1f8bd1982940e..966cd0e67e79d 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java @@ -31,12 +31,6 @@ @ConditionalOnBean(AppConfigurationRefresh.class) class AppConfigurationWebAutoConfiguration { - /** - * Listener for activity, to check for config store changes. - * - * @param appConfigurationRefresh Config Store refresher. - * @return Component for Listening for activity. - */ @Bean @ConditionalOnClass(RefreshEndpoint.class) AppConfigurationEventListener configListener(AppConfigurationRefresh appConfigurationRefresh) { @@ -50,23 +44,12 @@ AppConfigurationEventListener configListener(AppConfigurationRefresh appConfigur }) static class AppConfigurationPushRefreshConfiguration { - /** - * Creates Endpoint for push refresh. - * @param contextRefresher Spring Context Refresher - * @param appConfiguration App Configuration properties - * @return AppConfigurationRefreshEndpoint - */ @Bean public AppConfigurationRefreshEndpoint appConfigurationRefreshEndpoint(ContextRefresher contextRefresher, AppConfigurationProperties appConfiguration) { return new AppConfigurationRefreshEndpoint(contextRefresher, appConfiguration); } - /** - * Creates an Event Listener for push refresh events. - * @param appConfigurationRefresh App Configuration refresher. - * @return AppConfigurationRefreshEventListener - */ @Bean public AppConfigurationRefreshEventListener appConfigurationRefreshEventListener( AppConfigurationRefresh appConfigurationRefresh) { @@ -74,9 +57,6 @@ public AppConfigurationRefreshEventListener appConfigurationRefreshEventListener } } - /** - * Refresh from appconfiguration-refresh-bus - */ @Configuration @ConditionalOnClass(name = { "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties", @@ -85,25 +65,12 @@ public AppConfigurationRefreshEventListener appConfigurationRefreshEventListener "org.springframework.cloud.endpoint.RefreshEndpoint" }) static class AppConfigurationBusConfiguration { - /** - * Creates Endpoint for push bus refresh. - * @param context Spring Application Context - * @param bus Spring Bus properties - * @param appConfiguration App Configuration properties - * @param destinationFactory Spring destination factory - * @return AppConfigurationBusRefreshEndpoint - */ @Bean AppConfigurationBusRefreshEndpoint appConfigurationBusRefreshEndpoint(ApplicationContext context, BusProperties bus, AppConfigurationProperties appConfiguration, Destination.Factory destinationFactory) { return new AppConfigurationBusRefreshEndpoint(context, bus.getId(), destinationFactory, appConfiguration); } - /** - * Creates an Event Listener for push bus refresh events. - * @param appConfigurationRefresh App Configuration Refresher. - * @return AppConfigurationBusRefreshEventListener - */ @Bean AppConfigurationBusRefreshEventListener appConfigurationBusRefreshEventListener( AppConfigurationRefresh appConfigurationRefresh) { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefresh.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefresh.java index 7b0379b35fecb..9579d366934a6 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefresh.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefresh.java @@ -22,7 +22,7 @@ import com.azure.spring.cloud.config.AppConfigurationRefresh; import com.azure.spring.cloud.config.implementation.AppConfigurationRefreshUtil.RefreshEventData; import com.azure.spring.cloud.config.implementation.health.AppConfigurationStoreHealth; -import com.azure.spring.cloud.config.implementation.pipline.policies.BaseAppConfigurationPolicy; +import com.azure.spring.cloud.config.implementation.http.policy.BaseAppConfigurationPolicy; /** * Enables checking of Configuration updates. diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtil.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtil.java index 8776212b5463e..c6bf5d85dff49 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtil.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationRefreshUtil.java @@ -13,7 +13,7 @@ import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; -import com.azure.spring.cloud.config.implementation.pipline.policies.BaseAppConfigurationPolicy; +import com.azure.spring.cloud.config.implementation.http.policy.BaseAppConfigurationPolicy; import com.azure.spring.cloud.config.implementation.properties.AppConfigurationStoreMonitoring; import com.azure.spring.cloud.config.implementation.properties.FeatureFlagKeyValueSelector; import com.azure.spring.cloud.config.implementation.properties.FeatureFlagStore; diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java index 95c5535e32626..876eb22b531dd 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientsBuilder.java @@ -25,7 +25,7 @@ import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties; import com.azure.spring.cloud.autoconfigure.implementation.appconfiguration.AzureAppConfigurationProperties; import com.azure.spring.cloud.config.ConfigurationClientCustomizer; -import com.azure.spring.cloud.config.implementation.pipline.policies.BaseAppConfigurationPolicy; +import com.azure.spring.cloud.config.implementation.http.policy.BaseAppConfigurationPolicy; import com.azure.spring.cloud.config.implementation.properties.ConfigStore; import com.azure.spring.cloud.service.implementation.appconfiguration.ConfigurationClientBuilderFactory; diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/pipline/policies/BaseAppConfigurationPolicy.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicy.java similarity index 98% rename from sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/pipline/policies/BaseAppConfigurationPolicy.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicy.java index 76ce81bade122..0590ae0bc477c 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/pipline/policies/BaseAppConfigurationPolicy.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicy.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.implementation.pipline.policies; +package com.azure.spring.cloud.config.implementation.http.policy; import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.DEV_ENV_TRACING; import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.KEY_VAULT_CONFIGURED_TRACING; diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/pipline/policies/BaseAppConfigurationPolicyTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java similarity index 97% rename from sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/pipline/policies/BaseAppConfigurationPolicyTest.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java index e07be252a1ae9..db522880ce0b6 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/pipline/policies/BaseAppConfigurationPolicyTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.config.implementation.pipline.policies; +package com.azure.spring.cloud.config.implementation.http.policy; import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.CORRELATION_CONTEXT; import static com.azure.spring.cloud.config.implementation.AppConfigurationConstants.DEV_ENV_TRACING; @@ -27,6 +27,7 @@ import com.azure.core.http.HttpPipelineCallContext; import com.azure.core.http.HttpPipelineNextPolicy; import com.azure.core.http.HttpRequest; +import com.azure.spring.cloud.config.implementation.http.policy.BaseAppConfigurationPolicy; // This test class needs to be isolated and ran sequential as it uses BaseAppConfigurationPolicy.setWatchRequests // which mutates a global static and can result in race condition failures. diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/CHANGELOG.md b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/CHANGELOG.md similarity index 100% rename from sdk/spring/spring-cloud-azure-test-appconfiguration-config/CHANGELOG.md rename to sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/CHANGELOG.md diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/README.md b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/README.md similarity index 100% rename from sdk/spring/spring-cloud-azure-test-appconfiguration-config/README.md rename to sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/README.md diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/pom.xml similarity index 94% rename from sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml rename to sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/pom.xml index dcdd4bb9a0a23..ca80afc4c56c8 100644 --- a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/pom.xml @@ -13,8 +13,8 @@ com.azure.spring - spring-cloud-azure-test-appconfiguration-config - 1.0.0 + spring-cloud-azure-integration-test-appconfiguration-config + 1.0.0 true diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java similarity index 100% rename from sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java rename to sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java similarity index 100% rename from sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java rename to sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java similarity index 100% rename from sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java rename to sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MessageProperties.java b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MessageProperties.java similarity index 100% rename from sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MessageProperties.java rename to sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MessageProperties.java diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/resources/META-INF/spring.factories b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/resources/META-INF/spring.factories similarity index 100% rename from sdk/spring/spring-cloud-azure-test-appconfiguration-config/src/test/resources/META-INF/spring.factories rename to sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/resources/META-INF/spring.factories diff --git a/sdk/spring/spring-cloud-azure-test-appconfiguration-config/test-resources.json b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/test-resources.json similarity index 100% rename from sdk/spring/spring-cloud-azure-test-appconfiguration-config/test-resources.json rename to sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/test-resources.json diff --git a/sdk/spring/tests.yml b/sdk/spring/tests.yml index 05e5831491475..030e24863ab40 100644 --- a/sdk/spring/tests.yml +++ b/sdk/spring/tests.yml @@ -3,8 +3,8 @@ trigger: none stages: - template: ../../eng/pipelines/templates/stages/archetype-sdk-tests.yml parameters: - SupportedClouds: 'Public,UsGov,China' - Clouds: 'Public' + SupportedClouds: "Public,UsGov,China" + Clouds: "Public" CloudConfig: Public: SubscriptionConfiguration: $(sub-config-azure-cloud-test-resources) @@ -12,7 +12,7 @@ stages: SubscriptionConfiguration: $(sub-config-gov-test-resources) China: SubscriptionConfiguration: $(sub-config-cn-test-resources) - Location: 'chinanorth3' + Location: "chinanorth3" TestResourceDirectories: - spring/spring-cloud-azure-integration-tests/test-resources/common - spring/spring-cloud-azure-integration-tests/test-resources/jdbc/mysql @@ -23,6 +23,7 @@ stages: - spring/spring-cloud-azure-integration-tests/test-resources/storage - spring/spring-cloud-azure-integration-tests/test-resources/keyvault - spring/spring-cloud-azure-integration-tests/test-resources/dummy + - spring/spring-cloud-azure-integration-test-appconfiguration-config Artifacts: - name: spring-cloud-azure-integration-tests groupId: com.azure.spring @@ -30,5 +31,5 @@ stages: TimeoutInMinutes: 240 ServiceDirectory: spring TestName: SpringIntegrationTests - TestGoals: 'verify' - TestOptions: '-DskipSpringITs=false' + TestGoals: "verify" + TestOptions: "-DskipSpringITs=false" From a7ef8551bad2990cefce30b865d4211fb39a1613 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 13 Feb 2023 11:00:05 -0800 Subject: [PATCH 25/38] Fixing Auto Configuration and revapi --- .../src/main/resources/revapi/revapi.json | 5 ++ ...onfigurationConfigHealthConfiguration.java | 32 +++++++++++++ .../spring-cloud-azure-actuator/pom.xml | 6 +++ ...AppConfigurationConfigHealthIndicator.java | 47 +++++++++++++++++++ .../AppConfigurationWebAutoConfiguration.java | 4 +- .../spring/cloud/config/web/package-info.java | 6 +++ .../cloud/config/AppConfigurationRefresh.java | 4 +- .../AppConfigurationPullRefresh.java | 3 +- .../AppConfigurationReplicaClientFactory.java | 7 ++- .../BaseAppConfigurationPolicyTest.java | 1 - 10 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 sdk/spring/spring-cloud-azure-actuator-autoconfigure/src/main/java/com/azure/spring/cloud/actuator/autoconfigure/appconfiguration/AppConfigurationConfigHealthConfiguration.java create mode 100644 sdk/spring/spring-cloud-azure-actuator/src/main/java/com/azure/spring/cloud/actuator/appconfiguration/AppConfigurationConfigHealthIndicator.java create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java diff --git a/eng/code-quality-reports/src/main/resources/revapi/revapi.json b/eng/code-quality-reports/src/main/resources/revapi/revapi.json index 17a3c5566ac96..f8381fd1f3173 100644 --- a/eng/code-quality-reports/src/main/resources/revapi/revapi.json +++ b/eng/code-quality-reports/src/main/resources/revapi/revapi.json @@ -786,6 +786,11 @@ "code": "java.class.externalClassExposedInAPI", "new": "class com.azure.spring.cloud.feature.manager.FilterNotFoundException", "justification": "This is an Azure SDK class that is used in the public API." + }, + { + "code": "java.class.externalClassExposedInAPI", + "new": "interface com.azure.spring.cloud.config.AppConfigurationRefresh", + "justification": "This is an Azure SDK class that is used in the public API." } ] } diff --git a/sdk/spring/spring-cloud-azure-actuator-autoconfigure/src/main/java/com/azure/spring/cloud/actuator/autoconfigure/appconfiguration/AppConfigurationConfigHealthConfiguration.java b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/src/main/java/com/azure/spring/cloud/actuator/autoconfigure/appconfiguration/AppConfigurationConfigHealthConfiguration.java new file mode 100644 index 0000000000000..83679110fe0f3 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/src/main/java/com/azure/spring/cloud/actuator/autoconfigure/appconfiguration/AppConfigurationConfigHealthConfiguration.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.actuator.autoconfigure.appconfiguration; + +import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.azure.spring.cloud.actuator.appconfiguration.AppConfigurationConfigHealthIndicator; +import com.azure.spring.cloud.config.AppConfigurationRefresh; +import com.azure.spring.cloud.config.web.AppConfigurationWatchAutoConfiguration; + +/** + * Health Indicator for Azure App Configuration store connections. + */ +@Configuration +@ConditionalOnClass({ HealthIndicator.class }) +@ConditionalOnEnabledHealthIndicator("azure-app-configuration") +@AutoConfigureAfter(AppConfigurationWatchAutoConfiguration.class) +public class AppConfigurationConfigHealthConfiguration { + + @Bean + @ConditionalOnBean(AppConfigurationRefresh.class) + AppConfigurationConfigHealthIndicator appConfigurationHealthIndicator(AppConfigurationRefresh refresh) { + return new AppConfigurationConfigHealthIndicator(refresh); + } + +} \ No newline at end of file diff --git a/sdk/spring/spring-cloud-azure-actuator/pom.xml b/sdk/spring/spring-cloud-azure-actuator/pom.xml index dd8c7804d0803..f3ddb853a2236 100644 --- a/sdk/spring/spring-cloud-azure-actuator/pom.xml +++ b/sdk/spring/spring-cloud-azure-actuator/pom.xml @@ -102,6 +102,12 @@ 12.15.2 true + + com.azure.spring + spring-cloud-azure-appconfiguration-config-web + 4.0.0-beta.1 + true + org.springframework.boot diff --git a/sdk/spring/spring-cloud-azure-actuator/src/main/java/com/azure/spring/cloud/actuator/appconfiguration/AppConfigurationConfigHealthIndicator.java b/sdk/spring/spring-cloud-azure-actuator/src/main/java/com/azure/spring/cloud/actuator/appconfiguration/AppConfigurationConfigHealthIndicator.java new file mode 100644 index 0000000000000..6a419d54c3d47 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-actuator/src/main/java/com/azure/spring/cloud/actuator/appconfiguration/AppConfigurationConfigHealthIndicator.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.actuator.appconfiguration; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; + +import com.azure.spring.cloud.config.AppConfigurationRefresh; + +/** + * Indicator class of App Configuration + */ +public final class AppConfigurationConfigHealthIndicator implements HealthIndicator { + + private final AppConfigurationRefresh refresh; + + /** + * Indicator for the Health endpoint for connections to App Configurations. + * @param refresh App Configuration store refresher + */ + public AppConfigurationConfigHealthIndicator(AppConfigurationRefresh refresh) { + this.refresh = refresh; + } + + @Override + public Health health() { + Health.Builder healthBuilder = new Health.Builder(); + boolean healthy = true; + + for (String store : refresh.getAppConfigurationStoresHealth().keySet()) { + if ("DOWN".equals(refresh.getAppConfigurationStoresHealth().get(store))) { + healthy = false; + healthBuilder.withDetail(store, "DOWN"); + } else if ("NOT_LOADED".equals(refresh.getAppConfigurationStoresHealth().get(store))) { + healthBuilder.withDetail(store, "NOT LOADED"); + } else { + healthBuilder.withDetail(store, "UP"); + } + } + + if (!healthy) { + return healthBuilder.down().build(); + } + return healthBuilder.up().build(); + } + +} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java index 966cd0e67e79d..acdc69c0a5bb8 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/AppConfigurationWebAutoConfiguration.java @@ -45,13 +45,13 @@ AppConfigurationEventListener configListener(AppConfigurationRefresh appConfigur static class AppConfigurationPushRefreshConfiguration { @Bean - public AppConfigurationRefreshEndpoint appConfigurationRefreshEndpoint(ContextRefresher contextRefresher, + AppConfigurationRefreshEndpoint appConfigurationRefreshEndpoint(ContextRefresher contextRefresher, AppConfigurationProperties appConfiguration) { return new AppConfigurationRefreshEndpoint(contextRefresher, appConfiguration); } @Bean - public AppConfigurationRefreshEventListener appConfigurationRefreshEventListener( + AppConfigurationRefreshEventListener appConfigurationRefreshEventListener( AppConfigurationRefresh appConfigurationRefresh) { return new AppConfigurationRefreshEventListener(appConfigurationRefresh); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java new file mode 100644 index 0000000000000..4d153f142bd93 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + // Licensed under the MIT License. + /** + * Package contains classes for enabling auto refresh of Azure App Configuration stores + */ + package com.azure.spring.cloud.config.web; \ No newline at end of file diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationRefresh.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationRefresh.java index b44a372fc6638..a94299329e0c1 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationRefresh.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationRefresh.java @@ -8,8 +8,6 @@ import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.scheduling.annotation.Async; -import com.azure.spring.cloud.config.implementation.health.AppConfigurationStoreHealth; - /** * Enables checking of Configuration updates. */ @@ -38,6 +36,6 @@ public interface AppConfigurationRefresh extends ApplicationEventPublisherAware * * @return Map of String, endpoint, and Health information. */ - Map getAppConfigurationStoresHealth(); + Map getAppConfigurationStoresHealth(); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefresh.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefresh.java index 9579d366934a6..e0dc5b76d64ea 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefresh.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationPullRefresh.java @@ -21,7 +21,6 @@ import com.azure.spring.cloud.config.AppConfigurationRefresh; import com.azure.spring.cloud.config.implementation.AppConfigurationRefreshUtil.RefreshEventData; -import com.azure.spring.cloud.config.implementation.health.AppConfigurationStoreHealth; import com.azure.spring.cloud.config.implementation.http.policy.BaseAppConfigurationPolicy; /** @@ -120,7 +119,7 @@ private boolean refreshStores() { } @Override - public Map getAppConfigurationStoresHealth() { + public Map getAppConfigurationStoresHealth() { return clientFactory.getHealth(); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactory.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactory.java index bf2a47f38343f..e00f432dc7d09 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactory.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/implementation/AppConfigurationReplicaClientFactory.java @@ -6,7 +6,6 @@ import java.util.List; import java.util.Map; -import com.azure.spring.cloud.config.implementation.health.AppConfigurationStoreHealth; import com.azure.spring.cloud.config.implementation.properties.ConfigStore; /** @@ -80,10 +79,10 @@ void backoffClientClient(String originEndpoint, String endpoint) { * Gets the health of the client connections to App Configuration * @return map of endpoint origin it's health */ - Map getHealth() { - Map health = new HashMap<>(); + Map getHealth() { + Map health = new HashMap<>(); - CONNECTIONS.forEach((key, value) -> health.put(key, value.getHealth())); + CONNECTIONS.forEach((key, value) -> health.put(key, value.getHealth().name())); return health; } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java index db522880ce0b6..bba64a81e2df3 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/implementation/http/policy/BaseAppConfigurationPolicyTest.java @@ -27,7 +27,6 @@ import com.azure.core.http.HttpPipelineCallContext; import com.azure.core.http.HttpPipelineNextPolicy; import com.azure.core.http.HttpRequest; -import com.azure.spring.cloud.config.implementation.http.policy.BaseAppConfigurationPolicy; // This test class needs to be isolated and ran sequential as it uses BaseAppConfigurationPolicy.setWatchRequests // which mutates a global static and can result in race condition failures. From cfa2b8272f1014f137dbbfda5314f006b8af45a5 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 13 Feb 2023 11:45:15 -0800 Subject: [PATCH 26/38] Updating RevApi --- .../AzureSdkAllowedExternalApis.java | 3 ++ .../src/main/resources/revapi/revapi.json | 50 ------------------- .../spring/cloud/config/web/package-info.java | 10 ++-- 3 files changed, 8 insertions(+), 55 deletions(-) diff --git a/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkAllowedExternalApis.java b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkAllowedExternalApis.java index 38c2e7f37d3b4..7ccfb570b1cab 100644 --- a/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkAllowedExternalApis.java +++ b/eng/code-quality-reports/src/main/java/com/azure/tools/revapi/transforms/AzureSdkAllowedExternalApis.java @@ -75,12 +75,15 @@ private static ExternalApiStatus shouldExternalApiBeIgnored(TypeElement element) || "core.".regionMatches(0, className, 10, 5) || "cosmos.".regionMatches(0, className, 10, 7) || "data.schemaregistry.".regionMatches(0, className, 10, 20) + || "data.appconfiguration.".regionMatches(0, className, 10, 22) || "json.".regionMatches(0, className, 10, 5) || "messaging.eventgrid.".regionMatches(0, className, 10, 20) || "messaging.eventhubs.".regionMatches(0, className, 10, 20) || "messaging.servicebus.".regionMatches(0, className, 10, 21) || "resourcemanager.".regionMatches(0, className, 10, 16) || "security.keyvault.".regionMatches(0, className, 10, 18) + || "spring.cloud.config.".regionMatches(0, className, 10, 20) + || "spring.cloud.feature.".regionMatches(0, className, 10, 21) || "storage.".regionMatches(0, className, 10, 8)) { return ExternalApiStatus.SDK_CLASSES; } else if ("perf.test.core.".regionMatches(0, className, 10, 15)) { diff --git a/eng/code-quality-reports/src/main/resources/revapi/revapi.json b/eng/code-quality-reports/src/main/resources/revapi/revapi.json index 862f33bdc7043..4dfd489e8f875 100644 --- a/eng/code-quality-reports/src/main/resources/revapi/revapi.json +++ b/eng/code-quality-reports/src/main/resources/revapi/revapi.json @@ -403,56 +403,6 @@ "old": "field com.azure.messaging.eventgrid.SystemEventNames.SERVICE_BUS_DEADLETTER_MESSAGES_AVAILABLE_WITH_NO_LISTENER", "new": "field com.azure.messaging.eventgrid.SystemEventNames.SERVICE_BUS_DEADLETTER_MESSAGES_AVAILABLE_WITH_NO_LISTENER", "justification": "Previous constant value had a typo and was never functional." - }, - { - "code": "java.class.externalClassExposedInAPI", - "new": "class com.azure.data.appconfiguration.models.SettingSelector", - "justification": "This is an Azure SDK class that is used in the public API." - }, - { - "code": "java.class.externalClassExposedInAPI", - "new": "class com.azure.data.appconfiguration.models.SettingFields", - "justification": "This is an Azure SDK class that is used in the public API." - }, - { - "code": "java.class.externalClassExposedInAPI", - "new": "class com.azure.data.appconfiguration.models.ConfigurationSetting", - "justification": "This is an Azure SDK class that is used in the public API." - }, - { - "code": "java.class.externalClassExposedInAPI", - "new": "enum com.azure.data.appconfiguration.ConfigurationServiceVersion", - "justification": "This is an Azure SDK class that is used in the public API." - }, - { - "code": "java.class.externalClassExposedInAPI", - "new": "class com.azure.data.appconfiguration.ConfigurationClientBuilder", - "justification": "This is an Azure SDK class that is used in the public API." - }, - { - "code": "java.class.externalClassExposedInAPI", - "new": "class com.azure.data.appconfiguration.ConfigurationClient", - "justification": "This is an Azure SDK class that is used in the public API." - }, - { - "code": "java.class.externalClassExposedInAPI", - "new": "class com.azure.data.appconfiguration.ConfigurationAsyncClient", - "justification": "This is an Azure SDK class that is used in the public API." - }, - { - "code": "java.class.externalClassExposedInAPI", - "new": "class com.azure.spring.cloud.feature.manager.FeatureManager", - "justification": "This is an Azure SDK class that is used in the public API." - }, - { - "code": "java.class.externalClassExposedInAPI", - "new": "class com.azure.spring.cloud.feature.manager.FilterNotFoundException", - "justification": "This is an Azure SDK class that is used in the public API." - }, - { - "code": "java.class.externalClassExposedInAPI", - "new": "interface com.azure.spring.cloud.config.AppConfigurationRefresh", - "justification": "This is an Azure SDK class that is used in the public API." } ] } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java index 4d153f142bd93..efdf0c7f84347 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/config/web/package-info.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. - // Licensed under the MIT License. - /** - * Package contains classes for enabling auto refresh of Azure App Configuration stores - */ - package com.azure.spring.cloud.config.web; \ No newline at end of file +// Licensed under the MIT License. +/** + * Package contains classes for enabling auto refresh of Azure App Configuration stores + */ +package com.azure.spring.cloud.config.web; From a9067524e66aec0a2626bbf12e986e4ae99b1fa6 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 13 Feb 2023 12:25:33 -0800 Subject: [PATCH 27/38] Fixing AutoConfiguration --- .../spring-cloud-azure-actuator-autoconfigure/pom.xml | 6 ++++++ .../AppConfigurationConfigHealthConfiguration.java | 4 ++-- .../web/AppConfigurationWebAutoConfigurationTest.java | 2 +- .../config => }/AppConfigurationAutoConfiguration.java | 5 ++--- 4 files changed, 11 insertions(+), 6 deletions(-) rename sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/{implementation/config => }/AppConfigurationAutoConfiguration.java (90%) diff --git a/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml index 44c95a95fb778..bf152d0d03d2f 100644 --- a/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml +++ b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml @@ -50,6 +50,12 @@ spring-cloud-azure-autoconfigure 4.7.0-beta.1 + + com.azure.spring + spring-cloud-azure-appconfiguration-config-web + 4.0.0-beta.1 + true + - com.azure.spring - spring-cloud-azure-appconfiguration-config-web - 4.0.0-beta.1 - true - + com.azure.spring + spring-cloud-azure-appconfiguration-config-web + 4.0.0-beta.1 + - true - + com.azure.spring + spring-cloud-azure-appconfiguration-config-web + 4.0.0-beta.1 + org.springframework.boot From 4ac3db2e60d4961ea60d3a2d03cae231a22d1303 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 13 Feb 2023 12:50:52 -0800 Subject: [PATCH 29/38] Wrong dependency --- sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index 3e915c92ea1ed..772efb24ec1e8 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -61,7 +61,7 @@ com.azure.spring - spring-cloud-azure-actuator-autoconfigure + spring-cloud-azure-autoconfigure 4.6.0 From 1c6a5fe6fa30b17ca6309ff46025708dd6ce877a Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 13 Feb 2023 13:08:28 -0800 Subject: [PATCH 30/38] Update pom.xml --- .../spring-cloud-azure-appconfiguration-config/pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index 772efb24ec1e8..7f9795395fc09 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -33,6 +33,12 @@ spring-cloud-context 3.1.5 + + org.springframework.boot + spring-boot-actuator + 2.7.8 + compile + com.azure @@ -101,6 +107,7 @@ com.fasterxml.jackson.core:jackson-databind:[2.13.4.2] javax.annotation:javax.annotation-api:[1.3.2] org.hibernate.validator:hibernate-validator:[6.2.5.Final] + org.springframework.boot:spring-boot-actuator:[2.7.8] org.springframework.boot:spring-boot-autoconfigure:[2.7.8] org.springframework.cloud:spring-cloud-context:[3.1.5] org.springframework.cloud:spring-cloud-starter-bootstrap:[3.1.5] From a7a40eb7f1621f8d19e8edf41807b0a4cef68e16 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 13 Feb 2023 13:16:35 -0800 Subject: [PATCH 31/38] Update pom.xml --- sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index 7f9795395fc09..c53efbaccf5be 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -68,7 +68,7 @@ com.azure.spring spring-cloud-azure-autoconfigure - 4.6.0 + 4.6.0 From 61a985498e9daddf8e17d6809d23de9a78f2341a Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 13 Feb 2023 13:17:23 -0800 Subject: [PATCH 32/38] Update pom.xml --- sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index c53efbaccf5be..b870e54b6981e 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -68,7 +68,7 @@ com.azure.spring spring-cloud-azure-autoconfigure - 4.6.0 + 4.6.0 From d0f945338d46c98fc52cdd9795a4ee798db0b213 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 13 Feb 2023 13:17:50 -0800 Subject: [PATCH 33/38] Update pom.xml --- sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml index b870e54b6981e..b7e45bec6defe 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -107,7 +107,7 @@ com.fasterxml.jackson.core:jackson-databind:[2.13.4.2] javax.annotation:javax.annotation-api:[1.3.2] org.hibernate.validator:hibernate-validator:[6.2.5.Final] - org.springframework.boot:spring-boot-actuator:[2.7.8] + org.springframework.boot:spring-boot-actuator:[2.7.8] org.springframework.boot:spring-boot-autoconfigure:[2.7.8] org.springframework.cloud:spring-cloud-context:[3.1.5] org.springframework.cloud:spring-cloud-starter-bootstrap:[3.1.5] From 1dfe5cda8c11955c30af0d38617d4861d91057ae Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 14 Feb 2023 23:57:18 -0800 Subject: [PATCH 34/38] Moving files to impl, and adding optional --- sdk/appconfiguration/tests.yml | 2 -- .../AppConfigurationConfigHealthConfiguration.java | 4 ++-- .../autoconfigure/impl/appconfiguration/package-info.java | 7 +++++++ sdk/spring/spring-cloud-azure-actuator/pom.xml | 1 + 4 files changed, 10 insertions(+), 4 deletions(-) rename sdk/spring/spring-cloud-azure-actuator-autoconfigure/src/main/java/com/azure/spring/cloud/actuator/autoconfigure/{ => impl}/appconfiguration/AppConfigurationConfigHealthConfiguration.java (94%) create mode 100644 sdk/spring/spring-cloud-azure-actuator-autoconfigure/src/main/java/com/azure/spring/cloud/actuator/autoconfigure/impl/appconfiguration/package-info.java diff --git a/sdk/appconfiguration/tests.yml b/sdk/appconfiguration/tests.yml index bfd9d5a61a5ee..00a60bf76ab21 100644 --- a/sdk/appconfiguration/tests.yml +++ b/sdk/appconfiguration/tests.yml @@ -11,5 +11,3 @@ stages: AZURE_CLIENT_ID: $(aad-azure-sdk-test-client-id) AZURE_CLIENT_SECRET: $(aad-azure-sdk-test-client-secret) AZURE_TENANT_ID: $(aad-azure-sdk-test-tenant-id) - TestGoals: "verify" - TestOptions: "-DskipSpringITs=false" diff --git a/sdk/spring/spring-cloud-azure-actuator-autoconfigure/src/main/java/com/azure/spring/cloud/actuator/autoconfigure/appconfiguration/AppConfigurationConfigHealthConfiguration.java b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/src/main/java/com/azure/spring/cloud/actuator/autoconfigure/impl/appconfiguration/AppConfigurationConfigHealthConfiguration.java similarity index 94% rename from sdk/spring/spring-cloud-azure-actuator-autoconfigure/src/main/java/com/azure/spring/cloud/actuator/autoconfigure/appconfiguration/AppConfigurationConfigHealthConfiguration.java rename to sdk/spring/spring-cloud-azure-actuator-autoconfigure/src/main/java/com/azure/spring/cloud/actuator/autoconfigure/impl/appconfiguration/AppConfigurationConfigHealthConfiguration.java index 95827ce10f52d..cd6c2c77d7081 100644 --- a/sdk/spring/spring-cloud-azure-actuator-autoconfigure/src/main/java/com/azure/spring/cloud/actuator/autoconfigure/appconfiguration/AppConfigurationConfigHealthConfiguration.java +++ b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/src/main/java/com/azure/spring/cloud/actuator/autoconfigure/impl/appconfiguration/AppConfigurationConfigHealthConfiguration.java @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.actuator.autoconfigure.appconfiguration; +package com.azure.spring.cloud.actuator.autoconfigure.impl.appconfiguration; import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; import org.springframework.boot.actuate.health.HealthIndicator; @@ -10,8 +10,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.azure.spring.cloud.actuator.appconfiguration.AppConfigurationConfigHealthIndicator; import com.azure.spring.cloud.config.AppConfigurationAutoConfiguration.AppConfigurationWatchAutoConfiguration; +import com.azure.spring.cloud.actuator.appconfiguration.AppConfigurationConfigHealthIndicator; import com.azure.spring.cloud.config.AppConfigurationRefresh; /** diff --git a/sdk/spring/spring-cloud-azure-actuator-autoconfigure/src/main/java/com/azure/spring/cloud/actuator/autoconfigure/impl/appconfiguration/package-info.java b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/src/main/java/com/azure/spring/cloud/actuator/autoconfigure/impl/appconfiguration/package-info.java new file mode 100644 index 0000000000000..de96ed2b7b763 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/src/main/java/com/azure/spring/cloud/actuator/autoconfigure/impl/appconfiguration/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Spring Cloud Azure auto-configuration for actuator App Configuration concerns. + */ +package com.azure.spring.cloud.actuator.autoconfigure.impl.appconfiguration; diff --git a/sdk/spring/spring-cloud-azure-actuator/pom.xml b/sdk/spring/spring-cloud-azure-actuator/pom.xml index f1ac62f48e9aa..9009d05a19a5c 100644 --- a/sdk/spring/spring-cloud-azure-actuator/pom.xml +++ b/sdk/spring/spring-cloud-azure-actuator/pom.xml @@ -106,6 +106,7 @@ com.azure.spring spring-cloud-azure-appconfiguration-config-web 4.0.0-beta.1 + true From b5769955a6fbdc7bde338d3c87504af750c825f5 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 15 Feb 2023 00:36:50 -0800 Subject: [PATCH 35/38] Update tests.yml --- sdk/spring/tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/spring/tests.yml b/sdk/spring/tests.yml index 030e24863ab40..732c6cd8510f1 100644 --- a/sdk/spring/tests.yml +++ b/sdk/spring/tests.yml @@ -28,6 +28,9 @@ stages: - name: spring-cloud-azure-integration-tests groupId: com.azure.spring safeName: springcloudazureintegrationtests + - name: spring-cloud-azure-integration-test-appconfiguration-config + groupId: com.azure.spring + safeName: springcloudazureintegrationtestappconfigurationconfig TimeoutInMinutes: 240 ServiceDirectory: spring TestName: SpringIntegrationTests From 3b55ca3bac612e138f4ae5cdb4da3ec2c59c2048 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Wed, 15 Feb 2023 23:55:15 +0800 Subject: [PATCH 36/38] move app config to the first when provisioning resources --- sdk/spring/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/spring/tests.yml b/sdk/spring/tests.yml index 732c6cd8510f1..4d2d00330a683 100644 --- a/sdk/spring/tests.yml +++ b/sdk/spring/tests.yml @@ -14,6 +14,7 @@ stages: SubscriptionConfiguration: $(sub-config-cn-test-resources) Location: "chinanorth3" TestResourceDirectories: + - spring/spring-cloud-azure-integration-test-appconfiguration-config - spring/spring-cloud-azure-integration-tests/test-resources/common - spring/spring-cloud-azure-integration-tests/test-resources/jdbc/mysql - spring/spring-cloud-azure-integration-tests/test-resources/appconfiguration @@ -23,7 +24,6 @@ stages: - spring/spring-cloud-azure-integration-tests/test-resources/storage - spring/spring-cloud-azure-integration-tests/test-resources/keyvault - spring/spring-cloud-azure-integration-tests/test-resources/dummy - - spring/spring-cloud-azure-integration-test-appconfiguration-config Artifacts: - name: spring-cloud-azure-integration-tests groupId: com.azure.spring From 71ba16fde5eb4764ab2925921f788cc09e3fed47 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 15 Feb 2023 09:40:01 -0800 Subject: [PATCH 37/38] Fixing Integration Test --- .../java/com/azure/spring/cloud/config/CustomClient.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java index 22e8a5410c30b..3e0de8858110a 100644 --- a/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java +++ b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java @@ -3,8 +3,9 @@ import com.azure.core.credential.TokenCredential; import com.azure.data.appconfiguration.ConfigurationClientBuilder; import com.azure.identity.EnvironmentCredentialBuilder; +import com.azure.security.keyvault.secrets.SecretClientBuilder; -public class CustomClient implements ConfigurationClientCustomizer { +public class CustomClient implements ConfigurationClientCustomizer, SecretClientCustomizer { TokenCredential buildCredential() { return new EnvironmentCredentialBuilder().build(); @@ -15,4 +16,9 @@ public void customize(ConfigurationClientBuilder builder, String endpoint) { builder.credential(buildCredential()); } + @Override + public void customize(SecretClientBuilder builder, String endpoint) { + builder.credential(buildCredential()); + } + } \ No newline at end of file From 7e6488cdf13907938af6a5038cf113c338e2705c Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 15 Feb 2023 10:51:51 -0800 Subject: [PATCH 38/38] Fixing Integration Tests --- .../src/main/resources/META-INF/spring.factories | 2 +- .../azure/spring/cloud/config/LoadConfigsTest.java | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories index 66ca567069c5c..b95a107ef1217 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/resources/META-INF/spring.factories @@ -1,5 +1,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -com.azure.spring.cloud.config.implementation.config.AppConfigurationAutoConfiguration +com.azure.spring.cloud.config.AppConfigurationAutoConfiguration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ com.azure.spring.cloud.config.implementation.config.AppConfigurationBootstrapConfiguration diff --git a/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java index 8cd2c5256988b..51be999a27143 100644 --- a/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java +++ b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java @@ -3,11 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.LinkedHashMap; - import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; @@ -23,21 +19,17 @@ @EnableConfigurationProperties(value = MessageProperties.class) public class LoadConfigsTest { - private final Logger log = LoggerFactory.getLogger(LoadConfigsTest.class); - @Autowired private MessageProperties properties; @Autowired private Environment env; + @SuppressWarnings("null") @Test public void sampleTest() { assertEquals("Test", properties.getMessage()); assertEquals("From Key Vault", properties.getSecret()); - - @SuppressWarnings("unchecked") - LinkedHashMap map = env.getProperty("feature-management.featureManagement", LinkedHashMap.class); - assertTrue(map.get("Alpha")); + assertTrue(env.getProperty("feature-management.featureManagement.Alpha", Boolean.class)); } }