Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-Reload for SecretsManager & ParameterStore property sources #536

Merged
merged 22 commits into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from 8 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
17 changes: 17 additions & 0 deletions spring-cloud-aws-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,23 @@
<artifactId>micrometer-registry-cloudwatch2</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<optional>true</optional>
</dependency>

<!-- AWS SDK v1 is required by testcontainers-localstack -->
<dependency>
<groupId>com.amazonaws</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
package io.awspring.cloud.autoconfigure.config.parameterstore;

import io.awspring.cloud.autoconfigure.AwsClientProperties;
import io.awspring.cloud.autoconfigure.config.reload.ReloadProperties;
import io.awspring.cloud.autoconfigure.config.secretsmanager.ReloadableProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;

/**
* Configuration properties for the AWS Parameter Store integration. Mostly based on the Spring Cloud Consul
Expand All @@ -28,11 +31,37 @@
* @since 2.0.0
*/
@ConfigurationProperties(ParameterStoreProperties.CONFIG_PREFIX)
public class ParameterStoreProperties extends AwsClientProperties {
public class ParameterStoreProperties extends AwsClientProperties implements ReloadableProperties {

/**
* Configuration prefix.
*/
public static final String CONFIG_PREFIX = "spring.cloud.aws.parameterstore";

/**
* Properties related to configuration reload.
*/
@NestedConfigurationProperty
private ReloadProperties reload = new ReloadProperties();

private boolean monitored;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this should be moved to ReloadProperties.


@Override
public boolean isMonitored() {
return monitored;
}

public void setMonitored(boolean monitored) {
this.monitored = monitored;
}

@Override
public ReloadProperties getReload() {
return reload;
}

public void setReload(ReloadProperties reload) {
this.reload = reload;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2013-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.awspring.cloud.autoconfigure.config.parameterstore;

import io.awspring.cloud.autoconfigure.config.reload.ConfigurationChangeDetector;
import io.awspring.cloud.autoconfigure.config.reload.ConfigurationUpdateStrategy;
import io.awspring.cloud.autoconfigure.config.reload.PollingAwsPropertySourceChangeDetector;
import io.awspring.cloud.parameterstore.ParameterStorePropertySource;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
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.cloud.autoconfigure.RefreshAutoConfiguration;
import org.springframework.cloud.autoconfigure.RefreshEndpointAutoConfiguration;
import org.springframework.cloud.commons.util.TaskSchedulerWrapper;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.cloud.context.restart.RestartEndpoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

@Configuration(proxyBeanMethods = false)
maciejwalkowiak marked this conversation as resolved.
Show resolved Hide resolved
@EnableConfigurationProperties(ParameterStoreProperties.class)
@ConditionalOnClass({ EndpointAutoConfiguration.class, RestartEndpoint.class, ContextRefresher.class })
@AutoConfigureAfter({ InfoEndpointAutoConfiguration.class, RefreshEndpointAutoConfiguration.class,
RefreshAutoConfiguration.class })
@ConditionalOnProperty(value = ParameterStoreProperties.CONFIG_PREFIX + ".monitored", havingValue = "true")
public class ParameterStoreReloadAutoConfiguration {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ConditionalOnBean(ContextRefresher.class)


@Bean("parameterStoreTaskScheduler")
@ConditionalOnMissingBean
public TaskSchedulerWrapper<TaskScheduler> taskScheduler() {
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();

threadPoolTaskScheduler.setThreadNamePrefix("spring-cloud-aws-parameterstore-ThreadPoolTaskScheduler-");
threadPoolTaskScheduler.setDaemon(true);

return new TaskSchedulerWrapper<>(threadPoolTaskScheduler);
}

@Bean
@ConditionalOnMissingBean
public ConfigurationUpdateStrategy parameterStoreConfigurationUpdateStrategy(ParameterStoreProperties properties,
Optional<RestartEndpoint> restarter, ContextRefresher refresher) {
return ConfigurationUpdateStrategy.create(properties.getReload(), refresher, restarter);
}

@Bean
@ConditionalOnBean(ConfigurationUpdateStrategy.class)
public ConfigurationChangeDetector<ParameterStorePropertySource> parameterStoreDataPropertyChangePollingWatcher(
ParameterStoreProperties properties, ConfigurationUpdateStrategy strategy,
@Qualifier("parameterStoreTaskScheduler") TaskSchedulerWrapper<TaskScheduler> taskScheduler,
ConfigurableEnvironment environment) {

return new PollingAwsPropertySourceChangeDetector<>(properties, ParameterStorePropertySource.class, strategy,
taskScheduler.getTaskScheduler(), environment);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2013-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.awspring.cloud.autoconfigure.config.reload;

import io.awspring.cloud.autoconfigure.config.secretsmanager.ReloadableProperties;
import io.awspring.cloud.core.config.AwsPropertySource;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.log.LogAccessor;

/**
* This is the superclass of all beans that can listen to changes in the configuration and fire a reload.
*
* Heavily inspired by Spring Cloud Kubernetes.
*
* @author Nicola Ferraro
* @author Matej Nedic
* @author Maciej Walkowiak
*/
public abstract class ConfigurationChangeDetector<T extends AwsPropertySource<?, ?>> {

private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(ConfigurationChangeDetector.class));

protected ReloadableProperties properties;

protected ConfigurationUpdateStrategy strategy;

protected ConfigurableEnvironment environment;

private final Class<T> propertySourceClass;

public ConfigurationChangeDetector(ReloadableProperties properties, ConfigurationUpdateStrategy strategy,
ConfigurableEnvironment environment, Class<T> propertySourceClass) {
this.properties = Objects.requireNonNull(properties);
this.strategy = Objects.requireNonNull(strategy);
this.environment = environment;
this.propertySourceClass = propertySourceClass;
}

public void reloadProperties() {
LOG.info(() -> "Reloading using strategy: " + this.strategy);
strategy.getReloadProcedure().run();
}

/**
* Determines if two property sources are different.
* @param left left map property sources
* @param right right map property sources
* @return {@code true} if source has changed
*/
protected boolean changed(EnumerablePropertySource<?> left, EnumerablePropertySource<?> right) {
if (left == right) {
return false;
}
for (String property : left.getPropertyNames()) {
if (!Objects.equals(left.getProperty(property), right.getProperty(property))) {
return true;
}
}
return false;
}

/**
* Returns a list of MapPropertySource that correspond to the current state of the system. This only handles the
* PropertySource objects that are returned.
* @param environment Spring environment
* @return a list of MapPropertySource that correspond to the current state of the system
*/
protected List<T> locateMapPropertySources(ConfigurableEnvironment environment) {

return environment.getPropertySources().stream()
.filter(it -> (it.getClass().isAssignableFrom(propertySourceClass))).map(it -> (T) it)
.collect(Collectors.toList());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2013-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.awspring.cloud.autoconfigure.config.reload;

import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.cloud.context.restart.RestartEndpoint;

/**
* This is the superclass of all named strategies that can be fired when the configuration changes.
*
* Heavily inspired by Spring Cloud Kubernetes.
*
* @author Nicola Ferraro
*/
public class ConfigurationUpdateStrategy {

private final ReloadStrategy reloadStrategy;
private final Runnable reloadProcedure;

public static ConfigurationUpdateStrategy create(ReloadProperties reloadProperties, ContextRefresher refresher,
Optional<RestartEndpoint> restarter) {
switch (reloadProperties.getStrategy()) {
case RESTART_CONTEXT:
restarter.orElseThrow(() -> new AssertionError("Restart endpoint is not enabled"));
return new ConfigurationUpdateStrategy(reloadProperties.getStrategy(), () -> {
wait(reloadProperties);
restarter.get().restart();
});
case REFRESH:
return new ConfigurationUpdateStrategy(reloadProperties.getStrategy(), refresher::refresh);
}
throw new IllegalStateException("Unsupported configuration update strategy: " + reloadProperties.getStrategy());
}

public ConfigurationUpdateStrategy(ReloadStrategy reloadStrategy, Runnable reloadProcedure) {
this.reloadStrategy = Objects.requireNonNull(reloadStrategy, "reloadStrategy cannot be null");
this.reloadProcedure = Objects.requireNonNull(reloadProcedure, "reloadProcedure cannot be null");
}

public ReloadStrategy getReloadStrategy() {
return reloadStrategy;
}

public Runnable getReloadProcedure() {
return reloadProcedure;
}

private static void wait(ReloadProperties properties) {
long waitMillis = ThreadLocalRandom.current().nextLong(properties.getMaxWaitForRestart().toMillis());
try {
Thread.sleep(waitMillis);
}
catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2013-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.awspring.cloud.autoconfigure.config.reload;

import io.awspring.cloud.autoconfigure.config.secretsmanager.ReloadableProperties;
import io.awspring.cloud.core.config.AwsPropertySource;
import java.util.List;
import javax.annotation.PostConstruct;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.support.PeriodicTrigger;

/**
* Configuration change detector that checks for changed configuration on a scheduled basis.
*
* Heavily inspired by Spring Cloud Kubernetes.
*
* @param <T> - property source class to check
* @author Matej Nedic
* @author Maciej Walkowiak
*/
public class PollingAwsPropertySourceChangeDetector<T extends AwsPropertySource<?, ?>>
extends ConfigurationChangeDetector<T> {

protected Log log = LogFactory.getLog(getClass());
private final TaskScheduler taskExecutor;

public PollingAwsPropertySourceChangeDetector(ReloadableProperties properties, Class<T> clazz,
ConfigurationUpdateStrategy strategy, TaskScheduler taskExecutor, ConfigurableEnvironment environment) {
super(properties, strategy, environment, clazz);
this.taskExecutor = taskExecutor;

}

@PostConstruct
private void init() {
log.info("Polling configurations change detector activated");
long period = properties.getReload().getPeriod().toMillis();
PeriodicTrigger trigger = new PeriodicTrigger(period);
trigger.setInitialDelay(period);
taskExecutor.schedule(this::executeCycle, trigger);
}

public void executeCycle() {
if (this.properties.isMonitored()) {
if (log.isDebugEnabled()) {
log.debug("Polling for changes in secrets");
}
List<T> currentSecretSources = locateMapPropertySources(this.environment);
if (!currentSecretSources.isEmpty()) {
for (T propertySource : currentSecretSources) {
AwsPropertySource<?, ?> clone = propertySource.copy();
clone.init();
if (changed(propertySource, clone)) {
reloadProperties();
}
}
}
}
}
}
Loading