Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,49 @@
import com.azure.spring.cloud.appconfiguration.config.implementation.stores.AppConfigurationSecretClientManager;
import com.azure.spring.cloud.service.implementation.keyvault.secrets.SecretClientBuilderFactory;

/**
* Factory for creating and managing AppConfigurationSecretClientManager instances. This class caches clients per Key
* Vault host.
*/
class AppConfigurationKeyVaultClientFactory {

/**
* Cache of secret client managers by Key Vault host.
private final Map<String, AppConfigurationSecretClientManager> keyVaultClients;

/**
* Optional customizer for Key Vault secret clients.
*/
private final SecretClientCustomizer keyVaultClientProvider;

/**
* Optional provider for custom secret resolution.
*/
private final KeyVaultSecretProvider keyVaultSecretProvider;

/**
* Factory for creating secret client builders.
*/
private final SecretClientBuilderFactory secretClientFactory;


/**
* Flag indicating whether credentials are configured.
*/
private final boolean credentialsConfigured;

/**
* Flag indicating whether the factory being used for telemetry.
*/
private final boolean isConfigured;

/**
* Creates a new AppConfigurationKeyVaultClientFactory.
*
* @param keyVaultClientProvider optional customizer for Key Vault secret clients
* @param keyVaultSecretProvider optional provider for custom secret resolution
* @param secretClientFactory factory for creating secret client builders
* @param credentialsConfigured whether credentials are configured
*/
AppConfigurationKeyVaultClientFactory(SecretClientCustomizer keyVaultClientProvider,
KeyVaultSecretProvider keyVaultSecretProvider, SecretClientBuilderFactory secretClientFactory,
boolean credentialsConfigured) {
Expand All @@ -35,6 +64,12 @@ class AppConfigurationKeyVaultClientFactory {
isConfigured = keyVaultClientProvider != null || credentialsConfigured;
}

/**
* Gets or creates a secret client manager for the specified Key Vault host.
*
* @param host the Key Vault host endpoint
* @return the secret client manager for the host
*/
AppConfigurationSecretClientManager getClient(String host) {
// Check if we already have a client for this key vault, if not we will make
// one
Expand All @@ -46,7 +81,12 @@ AppConfigurationSecretClientManager getClient(String host) {
return keyVaultClients.get(host);
}

public boolean isConfigured() {
/**
* Returns if Key Vault is configured to be used.
*
* @return true if either a client provider is configured or credentials are configured
*/
boolean isConfigured() {
return isConfigured;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,40 +11,64 @@
import org.springframework.core.env.EnumerablePropertySource;

import com.azure.core.util.Context;
import com.azure.data.appconfiguration.ConfigurationClient;

/**
* Azure App Configuration PropertySource unique per Store Label(Profile) combo.
*
* Abstract base class for Azure App Configuration PropertySource implementations.
*
* <p>
* i.e. If connecting to 2 stores and have 2 labels set 4 AppConfigurationPropertySources need to be
* created.
* Each PropertySource is unique per Store-Label(Profile) combination. For example, if connecting to 2 stores with 2
* labels each, 4 AppConfigurationPropertySources need to be created.
* </p>
*/
abstract class AppConfigurationPropertySource extends EnumerablePropertySource<ConfigurationClient> {
abstract class AppConfigurationPropertySource extends EnumerablePropertySource<AppConfigurationReplicaClient> {

/**
protected final Map<String, Object> properties = new LinkedHashMap<>();

/**
* Client for communicating with Azure App Configuration service.
*/
protected final AppConfigurationReplicaClient replicaClient;

/**
* Creates a new AppConfigurationPropertySource.
*
* @param name the name of this property source, should be unique to identify the store-label combination
* @param replicaClient the client for communicating with Azure App Configuration
*/
AppConfigurationPropertySource(String name, AppConfigurationReplicaClient replicaClient) {
// The context alone does not uniquely define a PropertySource, append storeName
// and label to uniquely define a PropertySource
super(name);
this.replicaClient = replicaClient;
}

/**
* Returns the names of all properties in this property source.
*
* @return array of property names
*/
@Override
public String[] getPropertyNames() {
Set<String> keySet = properties.keySet();
return keySet.toArray(new String[keySet.size()]);
}

/**
* Returns the value of the specified property.
*
*/
@Override
public Object getProperty(String name) {
return properties.get(name);
}

/**
* Creates a comma-separated string from the given label filters.
*
* @param labelFilters array of label filters, may be null
* @return comma-separated string of labels, or empty string if null/empty
*/
protected static String getLabelName(String[] labelFilters) {
if (labelFilters == null) {
return "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,63 +20,92 @@
import reactor.core.publisher.Mono;

/**
* Enables checking of Configuration updates.
* Component responsible for checking Azure App Configuration for updates and triggering refresh events.
*/
@Component
public class AppConfigurationPullRefresh implements AppConfigurationRefresh {

private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationPullRefresh.class);

/**
* Flag to prevent concurrent refresh operations.
*/
private final AtomicBoolean running = new AtomicBoolean(false);

/**
* Publisher for Spring refresh events.
*/
private ApplicationEventPublisher publisher;

private final Long defaultMinBackoff = (long) 30;

/**
* Default minimum backoff duration in seconds when refresh operations fail.
*/
private static final Long DEFAULT_MIN_BACKOFF_SECONDS = 30L;

/**
* Factory for creating App Configuration replica clients.
*/
private final AppConfigurationReplicaClientFactory clientFactory;

/**
* Time interval between configuration refresh checks.
*/
private final Duration refreshInterval;


/**
* Component for replica lookup and failover functionality.
*/
private final ReplicaLookUp replicaLookUp;


/**
* Utility component for refresh operations.
*/
private final AppConfigurationRefreshUtil refreshUtils;

/**
* Component used for checking for and triggering configuration refreshes.
* Creates a new AppConfigurationPullRefresh component.
*
* @param clientFactory Clients stores used to connect to App Configuration. * @param defaultMinBackoff default
* @param refreshInterval time between refresh intervals
* @param clientFactory factory for creating App Configuration clients to connect to stores
* @param refreshInterval time duration between refresh interval checks
* @param replicaLookUp component for handling replica lookup and failover
* @param refreshUtils utility component for refresh operations
*/
public AppConfigurationPullRefresh(AppConfigurationReplicaClientFactory clientFactory, Duration refreshInterval,
ReplicaLookUp replicaLookUp, AppConfigurationRefreshUtil refreshUtils) {
this.refreshInterval = refreshInterval;
this.clientFactory = clientFactory;
this.replicaLookUp = replicaLookUp;
this.refreshUtils = refreshUtils;

}

/**
* Sets the Spring application event publisher for publishing refresh events.
*
* @param applicationEventPublisher the Spring event publisher to use for refresh events
*/
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}

/**
* Checks configurations to see if configurations should be reloaded. If the refresh interval has passed and a
* trigger has been updated configuration are reloaded.
*
* @return Future with a boolean of if a RefreshEvent was published. If refreshConfigurations is currently being run
* elsewhere this method will return right away as <b>false</b>.
* Checks configurations to see if they should be reloaded. If the refresh interval has passed and a trigger has
* been updated, configurations are reloaded.
*
* @return a Mono containing a boolean indicating if a RefreshEvent was published. Returns {@code false} if
* refreshConfigurations is currently being executed elsewhere.
*/
public Mono<Boolean> refreshConfigurations() {
return Mono.just(refreshStores());
}

/**
* Soft expires refresh interval. Sets amount of time to next refresh to be a random value between 0 and 15 seconds,
* unless value is less than the amount of time to the next refresh check.
* @param endpoint Config Store endpoint to expire refresh interval on.
* @param syncToken syncToken to verify the latest changes are available on pull
* unless that value is less than the amount of time to the next refresh check.
*
* @param endpoint the Config Store endpoint to expire refresh interval on
* @param syncToken the syncToken to verify the latest changes are available on pull
*/
public void expireRefreshInterval(String endpoint, String syncToken) {
LOGGER.debug("Expiring refresh interval for " + endpoint);
Expand All @@ -90,22 +119,23 @@ public void expireRefreshInterval(String endpoint, String syncToken) {

/**
* Goes through each config store and checks if any of its keys need to be refreshed. If any store has a value that
* needs to be updated a refresh event is called after every store is checked.
* needs to be updated, a refresh event is called after every store is checked.
*
* @return If a refresh event is called.
* @return true if a refresh event is published, false otherwise
*/
private boolean refreshStores() {
if (running.compareAndSet(false, true)) {
try {
RefreshEventData eventData = refreshUtils.refreshStoresCheck(clientFactory,
refreshInterval, defaultMinBackoff, replicaLookUp);
refreshInterval, DEFAULT_MIN_BACKOFF_SECONDS, replicaLookUp);
if (eventData.getDoRefresh()) {
publisher.publishEvent(new RefreshEvent(this, eventData, eventData.getMessage()));
return true;
}
} catch (Exception e) {
LOGGER.warn("Error occurred during configuration refresh, will retry at next interval", e);
// The next refresh will happen sooner if refresh interval is expired.
StateHolder.getCurrentState().updateNextRefreshTime(refreshInterval, defaultMinBackoff);
StateHolder.getCurrentState().updateNextRefreshTime(refreshInterval, DEFAULT_MIN_BACKOFF_SECONDS);
throw e;
} finally {
running.set(false);
Expand All @@ -114,6 +144,11 @@ private boolean refreshStores() {
return false;
}

/**
* Gets the health status of all configured App Configuration stores.
*
* @return a map containing the health status of each store, keyed by store identifier
*/
@Override
public Map<String, AppConfigurationStoreHealth> getAppConfigurationStoresHealth() {
return clientFactory.getHealth();
Expand Down
Loading