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/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/versioning/version_client.txt b/eng/versioning/version_client.txt index dff019c3ae4f3..533600779881d 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.1;1.2.0-beta.2 -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.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.6.0;4.7.0-beta.1 com.azure.spring:spring-messaging-azure;4.6.0;4.7.0-beta.1 com.azure.spring:spring-messaging-azure-eventhubs;4.6.0;4.7.0-beta.1 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/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/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/main/resources/appConfiguration.yaml b/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/resources/appConfiguration.yaml deleted file mode 100644 index 9654fca24e4e5..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-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 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/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/azure-spring-cloud-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MyCredentials.java b/sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/MyCredentials.java deleted file mode 100644 index dffdbdef437e6..0000000000000 --- a/sdk/appconfiguration/azure-spring-cloud-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 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..00a60bf76ab21 100644 --- a/sdk/appconfiguration/tests.yml +++ b/sdk/appconfiguration/tests.yml @@ -4,16 +4,10 @@ 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' 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..4541ce3359532 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-integration-test-appconfiguration-config + spring-cloud-azure-appconfiguration-config + spring-cloud-azure-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-integration-test-appconfiguration-config + spring-cloud-azure-appconfiguration-config + 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/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-actuator-autoconfigure/pom.xml b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml index 6ef662e4aff65..19611af5b275d 100644 --- a/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml +++ b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml @@ -50,6 +50,11 @@ 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 + org.springframework.boot diff --git a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/health/AppConfigurationHealthIndicator.java b/sdk/spring/spring-cloud-azure-actuator/src/main/java/com/azure/spring/cloud/actuator/appconfiguration/AppConfigurationConfigHealthIndicator.java similarity index 72% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/health/AppConfigurationHealthIndicator.java rename to sdk/spring/spring-cloud-azure-actuator/src/main/java/com/azure/spring/cloud/actuator/appconfiguration/AppConfigurationConfigHealthIndicator.java index 666d841aa7525..6a419d54c3d47 100644 --- a/sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/health/AppConfigurationHealthIndicator.java +++ b/sdk/spring/spring-cloud-azure-actuator/src/main/java/com/azure/spring/cloud/actuator/appconfiguration/AppConfigurationConfigHealthIndicator.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.actuator.appconfiguration; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; @@ -10,7 +10,7 @@ /** * Indicator class of App Configuration */ -public final class AppConfigurationHealthIndicator implements HealthIndicator { +public final class AppConfigurationConfigHealthIndicator implements HealthIndicator { private final AppConfigurationRefresh refresh; @@ -18,7 +18,7 @@ public final class AppConfigurationHealthIndicator implements HealthIndicator { * Indicator for the Health endpoint for connections to App Configurations. * @param refresh App Configuration store refresher */ - public AppConfigurationHealthIndicator(AppConfigurationRefresh refresh) { + public AppConfigurationConfigHealthIndicator(AppConfigurationRefresh refresh) { this.refresh = refresh; } @@ -28,10 +28,10 @@ public Health health() { boolean healthy = true; for (String store : refresh.getAppConfigurationStoresHealth().keySet()) { - if (AppConfigurationStoreHealth.DOWN.equals(refresh.getAppConfigurationStoresHealth().get(store))) { + if ("DOWN".equals(refresh.getAppConfigurationStoresHealth().get(store))) { healthy = false; healthBuilder.withDetail(store, "DOWN"); - } else if (refresh.getAppConfigurationStoresHealth().get(store).equals(AppConfigurationStoreHealth.NOT_LOADED)) { + } else if ("NOT_LOADED".equals(refresh.getAppConfigurationStoresHealth().get(store))) { healthBuilder.withDetail(store, "NOT LOADED"); } else { healthBuilder.withDetail(store, "UP"); 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 54% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config-web/README.md rename to sdk/spring/spring-cloud-azure-appconfiguration-config-web/README.md index dc6c4912a43fb..5cc8976aba684 100644 --- a/sdk/appconfiguration/azure-spring-cloud-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/spring-cloud-azure-starter-appconfiguration-config) 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..cb299f172ef85 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml @@ -0,0 +1,171 @@ + + + + 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.8 + + + org.springframework.boot + spring-boot-starter-actuator + 2.7.8 + true + + + org.springframework.cloud + spring-cloud-bus + 3.1.2 + true + + + 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] + + + + + + + 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 50% 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..acdc69c0a5bb8 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,86 +29,50 @@ @EnableConfigurationProperties(AppConfigurationProperties.class) @RemoteApplicationEventScan @ConditionalOnBean(AppConfigurationRefresh.class) -public class AppConfigurationWebAutoConfiguration { +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) - public AppConfigurationEventListener configListener(AppConfigurationRefresh appConfigurationRefresh) { + AppConfigurationEventListener configListener(AppConfigurationRefresh appConfigurationRefresh) { return new AppConfigurationEventListener(appConfigurationRefresh); } - /** - * Refresh from Pull Requests - */ @Configuration @ConditionalOnClass(name = { "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. - * @param contextRefresher Spring Context Refresher - * @param appConfiguration App Configuration properties - * @return AppConfigurationRefreshEndpoint - */ @Bean - public AppConfigurationRefreshEndpoint appConfigurationRefreshEndpoint(ContextRefresher contextRefresher, + 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( + AppConfigurationRefreshEventListener appConfigurationRefreshEventListener( AppConfigurationRefresh appConfigurationRefresh) { return new AppConfigurationRefreshEventListener(appConfigurationRefresh); } } - /** - * Refresh from appconfiguration-refresh-bus - */ @Configuration @ConditionalOnClass(name = { "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties", "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. - * @param context Spring Application Context - * @param bus Spring Bus properties - * @param appConfiguration App Configuration properties - * @param destinationFactory Spring destination factory - * @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); } - /** - * Creates an Event Listener for push bus refresh events. - * @param appConfigurationRefresh App Configuration Refresher. - * @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..1972b53e0d61a 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,15 +1,14 @@ // 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; 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/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 93% 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..d37b80b2635c6 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,18 +1,16 @@ // 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; 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/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 93% 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..095e571d25f16 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,18 +1,16 @@ // 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; 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); 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 82% 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..818b34b009bc4 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.autoconfigure.context.AzureGlobalPropertiesAutoConfiguration; import com.azure.spring.cloud.config.AppConfigurationAutoConfiguration; -import com.azure.spring.cloud.config.AppConfigurationBootstrapConfiguration; +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 53% rename from sdk/appconfiguration/azure-spring-cloud-appconfiguration-config/README.md rename to sdk/spring/spring-cloud-azure-appconfiguration-config/README.md index 9e2717e8dff0e..e80c622e8bf04 100644 --- a/sdk/appconfiguration/azure-spring-cloud-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/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 new file mode 100644 index 0000000000000..b7e45bec6defe --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml @@ -0,0 +1,134 @@ + + + + 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 + + + org.springframework.boot + spring-boot-actuator + 2.7.8 + compile + + + + 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-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-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] + + + + + + + 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/AppConfigurationAutoConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/config/AppConfigurationAutoConfiguration.java similarity index 81% 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/AppConfigurationAutoConfiguration.java index 6c7cb3d3a75d1..10fbd8cdb2e68 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/AppConfigurationAutoConfiguration.java @@ -12,8 +12,8 @@ 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,11 +28,11 @@ public class AppConfigurationAutoConfiguration { */ @Configuration @ConditionalOnClass(RefreshEndpoint.class) - static class AppConfigurationWatchAutoConfiguration { + public 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/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 91% 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..a94299329e0c1 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,8 +8,6 @@ import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.scheduling.annotation.Async; -import com.azure.spring.cloud.config.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/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 79% 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..e3b7855cb99b2 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,13 +8,13 @@ /** * 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. * @param builder ConfigurationClientBuilder * @param endpoint String */ - void setup(ConfigurationClientBuilder builder, String endpoint); + void customize(ConfigurationClientBuilder builder, String endpoint); } 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 72% 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..db0f3406a549f 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 customize(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..e0dc5b76d64ea 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,21 @@ 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.http.policy.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 +41,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 +101,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); @@ -116,8 +119,13 @@ private boolean refreshStores() { } @Override - public Map getAppConfigurationStoresHealth() { + 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..c6bf5d85dff49 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.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; + +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 86% 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..e00f432dc7d09 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,7 @@ 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.properties.ConfigStore; /** * Manages all client connections for all configuration stores. @@ -22,13 +20,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 +44,7 @@ public Map getConnections() { /** * @return the configStores */ - List getConfigStores() { + List getConfigStores() { // TODO (mametcal) This is never used? return configStores; } @@ -72,7 +71,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); } @@ -80,10 +79,10 @@ void backoffClient(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/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..876eb22b531dd --- /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.http.policy.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.customize(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/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..5e1c2fa9af05a --- /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,175 @@ +// 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.context.annotation.PropertySource; +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 +@PropertySource("classpath:appConfiguration.properties") +@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; + + @Bean + AppConfigurationPropertySourceLocator sourceLocator(AppConfigurationProperties properties, + AppConfigurationProviderProperties appProperties, AppConfigurationReplicaClientFactory clientFactory, + AppConfigurationKeyVaultClientFactory keyVaultClientFactory) + throws IllegalArgumentException { + + return new AppConfigurationPropertySourceLocator(appProperties, clientFactory, keyVaultClientFactory, + properties.getRefreshInterval(), properties.getStores()); + } + + @Bean + AppConfigurationKeyVaultClientFactory appConfigurationKeyVaultClientFactory(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/http/policy/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/http/policy/BaseAppConfigurationPolicy.java index 927f14ae06b7b..0590ae0bc477c 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/http/policy/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.http.policy; -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 57% 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..eb512f9d0bd95 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,55 +9,29 @@ 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. */ -@Validated @ConfigurationProperties(prefix = AppConfigurationProperties.CONFIG_PREFIX) -@Import({ AppConfigurationProviderProperties.class }) 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 +64,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 +92,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 77% 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..efb3e85062174 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,24 +1,18 @@ // 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; -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.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; -import org.springframework.validation.annotation.Validated; +import org.springframework.util.Assert; /** * Properties defining connection to Azure App Configuration. */ -@Configuration -@Validated -@PropertySource("classpath:appConfiguration.yaml") @ConfigurationProperties(prefix = AppConfigurationProviderProperties.CONFIG_PREFIX) public class AppConfigurationProviderProperties { @@ -26,29 +20,24 @@ 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(); - @NotEmpty + private static final Instant START_DATE = Instant.now(); + @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; @@ -112,7 +101,7 @@ public void setPrekillTime(int prekillTime) { * @return the startDate */ public Instant getStartDate() { - return startDate; + return START_DATE; } /** @@ -142,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."); + } } 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..e495280def37e --- /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 keyVaultClientProvider optional provider for overriding the Key Vault Client + * @param keyVaultSecretProvider optional provider for providing Secrets instead of connecting to Key Vault + * @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) { + 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.customize(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..b95a107ef1217 --- /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.AppConfigurationAutoConfiguration + +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +com.azure.spring.cloud.config.implementation.config.AppConfigurationBootstrapConfiguration + 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/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..b0bc41f3c63f1 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); @@ -179,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 @@ -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/http/policy/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/http/policy/BaseAppConfigurationPolicyTest.java index 5c68863d204b3..bba64a81e2df3 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/http/policy/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.http.policy; -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 59% 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..6d242c5237ebc 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/tree/main/sdk/spring/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 60% 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..11819b2783dea 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,22 +8,25 @@ 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 - test - org.springframework spring-web @@ -44,22 +45,13 @@ com.azure.spring - azure-spring-cloud-feature-management - 2.11.0-beta.1 + 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 @@ -73,7 +65,7 @@ - com.azure.spring:azure-spring-cloud-feature-management:[2.11.0-beta.1] + 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] 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 88% 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..9d0eaa0ed60c2 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 @@ -11,26 +11,24 @@ 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); - 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. @@ -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/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 88% 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..e85d56ef3052a 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 @@ -4,19 +4,17 @@ 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 { /** * 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 69% 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..d9e56afae0008 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,11 @@ import org.springframework.context.annotation.Configuration; import org.springframework.web.context.annotation.RequestScope; +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,35 +24,38 @@ public class FeatureManagementWebConfiguration { /** * Creates FeatureManagerSnapshot + * * @param featureManager App Configuration Feature Manager * @return FeatureManagerSnapshot */ @Bean @RequestScope - public FeatureManagerSnapshot featureManagerSnapshot(FeatureManager featureManager) { + FeatureManagerSnapshot featureManagerSnapshot(FeatureManager featureManager) { return new FeatureManagerSnapshot(featureManager); } /** * 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 * @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); } /** * 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 - 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/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/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/appconfiguration/azure-spring-cloud-feature-management/README.md b/sdk/spring/spring-cloud-azure-feature-management/README.md similarity index 84% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/README.md rename to sdk/spring/spring-cloud-azure-feature-management/README.md index 43f481ab7c533..d8020de013597 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/README.md +++ b/sdk/spring/spring-cloud-azure-feature-management/README.md @@ -1,8 +1,6 @@ # Spring Cloud for Azure feature management client library for Java -## Key concepts - -### Feature Management +## 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. @@ -14,60 +12,61 @@ Here are some of the benefits of using this library: * Feature Flag lifetime management * Configuration values can change in real-time, feature flags can be consistent across the entire request -### Feature Flags +## 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 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 +## 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 +## 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 + 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 +### Supported properties Name | Description | Required | Default ---|---|---|--- spring.cloud.azure.feature.management.fail-fast | Whether throw RuntimeException or not when exception occurs | No | true -### Consumption +## 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 +### 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. @@ -82,7 +81,7 @@ if(featureManager.isEnabledAsync("feature-t").block()) { `FeatureManager` can also be accessed by `@Component` classes. -#### Controllers +### 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. @@ -97,7 +96,7 @@ public String featureT() { The `featureT` endpoint can only be accessed if "feature-t" is enabled. -#### Disabled Action Handling +### 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`. @@ -114,13 +113,13 @@ public class DisabledFeaturesHandler implements IDisabledFeaturesHandler{ } ``` -#### Routing +### 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") -@FeatureGate(feature = "feature-t" fallback= "/oldEndpoint") +@FeatureGate(feature = "feature-t", fallback= "/oldEndpoint") @ResponseBody public String featureT() { ... @@ -133,7 +132,7 @@ public String oldEndpoint() { } ``` -### Implementing a Feature Filter +## 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. @@ -152,74 +151,77 @@ public class Random implements FeatureFilter { } ``` -#### Parameterized Feature Filters +### 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 +## 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 +## 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 +### 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 + feature-flags: + feature-v: + enabled-for: + - + name: PercentageFilter + parameters: + percentage-filter-setting: 50 ``` -#### TimeWindowFilter +### 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" + 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 +### 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 + feature-flags: + target: + enabled-for: + - + name: targetingFilter + parameters: + users: + - Jeff + - Alicia + groups: + - + name: Ring0 + rolloutPercentage: 100 + - + name: Ring1 + rolloutPercentage: 100 + defaultRolloutPercentage: 50 ``` -### Targeting +## 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. @@ -233,7 +235,7 @@ The following steps demonstrate an example of a progressive rollout for a new 'B 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 +### Targeting in an Application An example web application that uses the targeting feature filter is available in the [example project][example_project]. @@ -255,7 +257,7 @@ public class TargetingContextAccessor implements ITargetingContextAccessor { } ``` -#### Targeting Evaluation Options +### 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. @@ -266,12 +268,5 @@ Options are available to customize how targeting evaluation is performed across } ``` -## 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 +[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 81% rename from sdk/appconfiguration/azure-spring-cloud-feature-management/pom.xml rename to sdk/spring/spring-cloud-azure-feature-management/pom.xml index 0542e305c70c2..20d5e7a08b2dc 100644 --- a/sdk/appconfiguration/azure-spring-cloud-feature-management/pom.xml +++ b/sdk/spring/spring-cloud-azure-feature-management/pom.xml @@ -1,6 +1,6 @@ + xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> com.azure azure-client-sdk-parent @@ -10,14 +10,23 @@ 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 + + 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..e08394c360560 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/manager/FeatureManagementConfiguration.java @@ -0,0 +1,33 @@ +// 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.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); + } +} 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/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 80% 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..e063eb2cb0622 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 @@ -6,16 +6,14 @@ import org.slf4j.Logger; 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 +30,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 90% 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..151b7638f16e6 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 @@ -11,17 +11,15 @@ 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.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..0a28ef4b37fb8 --- /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,135 @@ +// 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 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. + */ +@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() + .setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE); + + 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; + + public FeatureManagementProperties() { + featureManagement = new HashMap<>(); + onOff = 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<>(); + + 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; + try { + feature = MAPPER.convertValue(featureValue, Feature.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 (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; + } + +} 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/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/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/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/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-test-appconfiguration-config/CHANGELOG.md b/sdk/spring/spring-cloud-azure-integration-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-integration-test-appconfiguration-config/CHANGELOG.md diff --git a/sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/README.md b/sdk/spring/spring-cloud-azure-integration-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-integration-test-appconfiguration-config/README.md diff --git a/sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/pom.xml similarity index 76% rename from sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/pom.xml rename to sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/pom.xml index c3f43ded4c3e8..ca80afc4c56c8 100644 --- a/sdk/appconfiguration/azure-spring-cloud-test-appconfiguration-config/pom.xml +++ b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/pom.xml @@ -13,8 +13,8 @@ com.azure.spring - azure-spring-cloud-test-appconfiguration-config - 1.0.0 + spring-cloud-azure-integration-test-appconfiguration-config + 1.0.0 true @@ -23,13 +23,8 @@ com.azure.spring - azure-spring-cloud-starter-appconfiguration-config - 2.12.0-beta.1 - - - com.microsoft.azure - azure - 1.34.0 + spring-cloud-azure-starter-appconfiguration-config + 4.0.0-beta.1 org.springframework.boot @@ -56,7 +51,8 @@ integration-test - ${skipSpringITs} + + ${skipSpringITs} @@ -66,7 +62,8 @@ maven-surefire-plugin 2.22.0 - ${skipSpringITs} + + ${skipSpringITs} 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-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java similarity index 72% 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-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/AppConfiguration.java index 7960c82f0c978..be90ab8f9400f 100644 --- 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-integration-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-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 new file mode 100644 index 0000000000000..3e0de8858110a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/CustomClient.java @@ -0,0 +1,24 @@ +package com.azure.spring.cloud.config; + +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, SecretClientCustomizer { + + TokenCredential buildCredential() { + return new EnvironmentCredentialBuilder().build(); + } + + @Override + 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 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-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java similarity index 76% 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-integration-test-appconfiguration-config/src/test/java/com/azure/spring/cloud/config/LoadConfigsTest.java index 8cd2c5256988b..51be999a27143 100644 --- 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-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)); } } 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-integration-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-integration-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/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/appconfiguration/azure-spring-cloud-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/appconfiguration/azure-spring-cloud-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/appconfiguration/azure-spring-cloud-test-appconfiguration-config/test-resources.json rename to sdk/spring/spring-cloud-azure-integration-test-appconfiguration-config/test-resources.json 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 89% 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..939ac2a5af79e 100644 --- a/sdk/appconfiguration/azure-spring-cloud-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.11.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.11.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. 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 62% 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..57de676187d73 100644 --- a/sdk/appconfiguration/azure-spring-cloud-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 - 2.12.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 - 2.12.0-beta.1 + spring-cloud-azure-appconfiguration-config-web + 4.0.0-beta.1 com.azure.spring - azure-spring-cloud-feature-management-web - 2.11.0-beta.1 + spring-cloud-azure-feature-management-web + 4.0.0-beta.3 - + 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/tests.yml b/sdk/spring/tests.yml index 05e5831491475..4d2d00330a683 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,8 +12,9 @@ stages: SubscriptionConfiguration: $(sub-config-gov-test-resources) China: SubscriptionConfiguration: $(sub-config-cn-test-resources) - Location: 'chinanorth3' + 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 @@ -27,8 +28,11 @@ 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 - TestGoals: 'verify' - TestOptions: '-DskipSpringITs=false' + TestGoals: "verify" + TestOptions: "-DskipSpringITs=false"