diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientAutoConfiguration.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientAutoConfiguration.java index 6560a4dd..b1c9744a 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientAutoConfiguration.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientAutoConfiguration.java @@ -21,6 +21,7 @@ import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnable import org.springframework.boot.actuate.health.HealthIndicator; 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.cloud.context.refresh.ContextRefresher; import org.springframework.context.ApplicationContext; @@ -43,6 +44,7 @@ import org.springframework.core.env.Environment; public class ConfigClientAutoConfiguration { @Bean + @ConditionalOnMissingBean public ConfigClientProperties configClientProperties(Environment environment, ApplicationContext context) { if (context.getParent() != null && BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context.getParent(), ConfigClientProperties.class).length > 0) { diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java index 6f619cb4..901bbf3e 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java @@ -44,6 +44,11 @@ public class ConfigClientProperties { */ public static final String PREFIX = "spring.cloud.config"; + /** + * Name of config discovery enabled property. + */ + public static final String CONFIG_DISCOVERY_ENABLED = PREFIX + ".discovery.enabled"; + /** * Token header name. */ diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java index c520edaa..f8cd69a8 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java @@ -73,6 +73,10 @@ public class ConfigServerConfigDataLoader implements ConfigDataLoader, Ordered { @@ -109,10 +110,10 @@ public class ConfigServerConfigDataLocationResolver return Collections.emptyList(); } - public List resolveProfileSpecific(ConfigDataLocationResolverContext context, - String location, boolean optional, Profiles profiles) { + public List resolveProfileSpecific( + ConfigDataLocationResolverContext resolverContext, String location, boolean optional, Profiles profiles) { - ConfigClientProperties properties = loadProperties(context.getBinder()); + ConfigClientProperties properties = loadProperties(resolverContext.getBinder()); String uris = (location.startsWith(getPrefix())) ? location.substring(getPrefix().length()) : location; @@ -121,15 +122,41 @@ public class ConfigServerConfigDataLocationResolver properties.setUri(uri); } - ConfigurableBootstrapContext bootstrapContext = context.getBootstrapContext(); + ConfigurableBootstrapContext bootstrapContext = resolverContext.getBootstrapContext(); bootstrapContext.registerIfAbsent(ConfigClientProperties.class, InstanceSupplier.of(properties)); bootstrapContext.addCloseListener(event -> event.getApplicationContext().getBeanFactory().registerSingleton( "configDataConfigClientProperties", event.getBootstrapContext().get(ConfigClientProperties.class))); - bootstrapContext.registerIfAbsent(RestTemplate.class, InstanceSupplier.from(() -> { - ConfigClientProperties props = bootstrapContext.get(ConfigClientProperties.class); + bootstrapContext.registerIfAbsent(RestTemplate.class, context -> { + ConfigClientProperties props = context.get(ConfigClientProperties.class); return createRestTemplate(props); - })); + }); + + boolean discoveryEnabled = resolverContext.getBinder().bind(CONFIG_DISCOVERY_ENABLED, Boolean.class) + .orElse(false); + + if (discoveryEnabled) { + // register ConfigServerInstanceMonitor + bootstrapContext.registerIfAbsent(ConfigServerInstanceMonitor.class, context -> { + ConfigServerInstanceProvider.Function function = context + .get(ConfigServerInstanceProvider.Function.class); + ConfigServerInstanceProvider instanceProvider = new ConfigServerInstanceProvider(function); + instanceProvider.setLog(log); + ConfigClientProperties clientProperties = context.get(ConfigClientProperties.class); + ConfigServerInstanceMonitor instanceMonitor = new ConfigServerInstanceMonitor(log, clientProperties, + instanceProvider); + instanceMonitor.setRefreshOnStartup(false); + instanceMonitor.refresh(); + return instanceMonitor; + }); + // promote ConfigServerInstanceMonitor to bean so updates can be made to config client uri + bootstrapContext.addCloseListener(event -> { + ConfigServerInstanceMonitor configServerInstanceMonitor = event.getBootstrapContext() + .get(ConfigServerInstanceMonitor.class); + event.getApplicationContext().getBeanFactory().registerSingleton("configServerInstanceMonitor", + configServerInstanceMonitor); + }); + } List locations = new ArrayList<>(); locations.add(new ConfigServerConfigDataLocation(properties, optional, profiles)); diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerInstanceMonitor.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerInstanceMonitor.java new file mode 100644 index 00000000..ce4f140e --- /dev/null +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerInstanceMonitor.java @@ -0,0 +1,139 @@ +/* + * Copyright 2013-2020 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 org.springframework.cloud.config.client; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.event.HeartbeatEvent; +import org.springframework.cloud.client.discovery.event.HeartbeatMonitor; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.SmartApplicationListener; +import org.springframework.util.Assert; + +final class ConfigServerInstanceMonitor implements SmartApplicationListener { + + private final Log log; + + private final ConfigClientProperties config; + + private final ConfigServerInstanceProvider instanceProvider; + + private final HeartbeatMonitor monitor = new HeartbeatMonitor(); + + /** + * If bootstrap, this should be true, for config data false. + */ + private boolean refreshOnStartup = true; + + ConfigServerInstanceMonitor(Log log, ConfigClientProperties config, ConfigServerInstanceProvider instanceProvider) { + this.log = log; + this.config = config; + this.instanceProvider = instanceProvider; + } + + void setRefreshOnStartup(boolean refreshOnStartup) { + this.refreshOnStartup = refreshOnStartup; + } + + @Override + public boolean supportsEventType(Class eventType) { + return ContextRefreshedEvent.class.isAssignableFrom(eventType) + || HeartbeatEvent.class.isAssignableFrom(eventType); + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (event instanceof ContextRefreshedEvent) { + startup((ContextRefreshedEvent) event); + } + else if (event instanceof HeartbeatEvent) { + heartbeat((HeartbeatEvent) event); + } + } + + public void startup(ContextRefreshedEvent event) { + if (refreshOnStartup) { + refresh(); + } + } + + public void heartbeat(HeartbeatEvent event) { + if (this.monitor.update(event.getValue())) { + refresh(); + } + } + + void refresh() { + try { + String serviceId = this.config.getDiscovery().getServiceId(); + Assert.hasText(serviceId, () -> ConfigClientProperties.PREFIX + ".service-id may not be null or empty"); + List listOfUrls = new ArrayList<>(); + List serviceInstances = this.instanceProvider.getConfigServerInstances(serviceId); + + for (int i = 0; i < serviceInstances.size(); i++) { + + ServiceInstance server = serviceInstances.get(i); + String url = getHomePage(server); + + if (server.getMetadata().containsKey("password")) { + String user = server.getMetadata().get("user"); + user = user == null ? "user" : user; + this.config.setUsername(user); + String password = server.getMetadata().get("password"); + this.config.setPassword(password); + } + + if (server.getMetadata().containsKey("configPath")) { + String path = server.getMetadata().get("configPath"); + if (url.endsWith("/") && path.startsWith("/")) { + url = url.substring(0, url.length() - 1); + } + url = url + path; + } + + listOfUrls.add(url); + } + + if (log.isDebugEnabled()) { + log.debug("Updating config uris to " + listOfUrls); + } + + String[] uri = new String[listOfUrls.size()]; + uri = listOfUrls.toArray(uri); + this.config.setUri(uri); + + } + catch (Exception ex) { + if (this.config.isFailFast()) { + throw ex; + } + else if (log.isWarnEnabled()) { + log.warn("Could not locate configserver via discovery", ex); + } + } + } + + private String getHomePage(ServiceInstance server) { + return server.getUri().toString() + "/"; + } + +} diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerInstanceProvider.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerInstanceProvider.java index 1fb2eaaf..2c52d3af 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerInstanceProvider.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerInstanceProvider.java @@ -32,10 +32,11 @@ import org.springframework.retry.annotation.Retryable; */ public class ConfigServerInstanceProvider { - private static Log logger = LogFactory.getLog(ConfigServerInstanceProvider.class); + private Log log = LogFactory.getLog(getClass()); private final Function function; + @Deprecated public ConfigServerInstanceProvider(DiscoveryClient client) { this.function = client::getInstances; } @@ -44,15 +45,23 @@ public class ConfigServerInstanceProvider { this.function = function; } + void setLog(Log log) { + this.log = log; + } + @Retryable(interceptor = "configServerRetryInterceptor") public List getConfigServerInstances(String serviceId) { - logger.debug("Locating configserver (" + serviceId + ") via discovery"); + if (log.isDebugEnabled()) { + log.debug("Locating configserver (" + serviceId + ") via discovery"); + } List instances = this.function.apply(serviceId); if (instances.isEmpty()) { throw new IllegalStateException("No instances found of configserver (" + serviceId + ")"); } - logger.debug( - "Located configserver (" + serviceId + ") via discovery. No of instances found: " + instances.size()); + if (log.isDebugEnabled()) { + log.debug("Located configserver (" + serviceId + ") via discovery. No of instances found: " + instances + .size()); + } return instances; } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/DiscoveryClientConfigServiceBootstrapConfiguration.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/DiscoveryClientConfigServiceBootstrapConfiguration.java index 78a3477e..a3bc8b47 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/DiscoveryClientConfigServiceBootstrapConfiguration.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/DiscoveryClientConfigServiceBootstrapConfiguration.java @@ -16,26 +16,16 @@ package org.springframework.cloud.config.client; -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; -import org.springframework.cloud.client.discovery.event.HeartbeatEvent; -import org.springframework.cloud.client.discovery.event.HeartbeatMonitor; import org.springframework.cloud.commons.util.UtilAutoConfiguration; -import org.springframework.context.ApplicationEvent; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.context.event.SmartApplicationListener; /** * Bootstrap configuration for a config client that wants to lookup the config server via @@ -43,14 +33,12 @@ import org.springframework.context.event.SmartApplicationListener; * * @author Dave Syer */ -@ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled", matchIfMissing = false) +@ConditionalOnProperty(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED) @Configuration(proxyBeanMethods = false) @Import({ UtilAutoConfiguration.class }) @EnableDiscoveryClient public class DiscoveryClientConfigServiceBootstrapConfiguration { - private static Log logger = LogFactory.getLog(DiscoveryClientConfigServiceBootstrapConfiguration.class); - @Bean public ConfigServerInstanceProvider configServerInstanceProvider( ObjectProvider function, @@ -61,105 +49,16 @@ public class DiscoveryClientConfigServiceBootstrapConfiguration { } DiscoveryClient client = discoveryClient.getIfAvailable(); if (client == null) { - throw new IllegalStateException("ConfigServerInstanceProvider reqiures a DiscoveryClient or Function"); + throw new IllegalStateException("ConfigServerInstanceProvider requires a DiscoveryClient or Function"); } - return new ConfigServerInstanceProvider(client); + return new ConfigServerInstanceProvider(client::getInstances); } @Bean - public SmartApplicationListener heartbeatListener(ConfigClientProperties properties, + public ConfigServerInstanceMonitor configServerInstanceMonitor(ConfigClientProperties properties, ConfigServerInstanceProvider provider) { - return new HeartbeatListener(properties, provider); - } - - private final static class HeartbeatListener implements SmartApplicationListener { - - private final ConfigClientProperties config; - - private final ConfigServerInstanceProvider instanceProvider; - - private final HeartbeatMonitor monitor = new HeartbeatMonitor(); - - private HeartbeatListener(ConfigClientProperties config, ConfigServerInstanceProvider instanceProvider) { - this.config = config; - this.instanceProvider = instanceProvider; - } - - @Override - public boolean supportsEventType(Class eventType) { - return ContextRefreshedEvent.class.isAssignableFrom(eventType) - || HeartbeatEvent.class.isAssignableFrom(eventType); - } - - @Override - public void onApplicationEvent(ApplicationEvent event) { - if (event instanceof ContextRefreshedEvent) { - startup((ContextRefreshedEvent) event); - } - else if (event instanceof HeartbeatEvent) { - heartbeat((HeartbeatEvent) event); - } - } - - public void startup(ContextRefreshedEvent event) { - refresh(); - } - - public void heartbeat(HeartbeatEvent event) { - if (this.monitor.update(event.getValue())) { - refresh(); - } - } - - private void refresh() { - try { - String serviceId = this.config.getDiscovery().getServiceId(); - List listOfUrls = new ArrayList<>(); - List serviceInstances = this.instanceProvider.getConfigServerInstances(serviceId); - - for (int i = 0; i < serviceInstances.size(); i++) { - - ServiceInstance server = serviceInstances.get(i); - String url = getHomePage(server); - - if (server.getMetadata().containsKey("password")) { - String user = server.getMetadata().get("user"); - user = user == null ? "user" : user; - this.config.setUsername(user); - String password = server.getMetadata().get("password"); - this.config.setPassword(password); - } - - if (server.getMetadata().containsKey("configPath")) { - String path = server.getMetadata().get("configPath"); - if (url.endsWith("/") && path.startsWith("/")) { - url = url.substring(0, url.length() - 1); - } - url = url + path; - } - - listOfUrls.add(url); - } - - String[] uri = new String[listOfUrls.size()]; - uri = listOfUrls.toArray(uri); - this.config.setUri(uri); - - } - catch (Exception ex) { - if (this.config.isFailFast()) { - throw ex; - } - else { - logger.warn("Could not locate configserver via discovery", ex); - } - } - } - - private String getHomePage(ServiceInstance server) { - return server.getUri().toString() + "/"; - } - + return new ConfigServerInstanceMonitor(LogFactory.getLog(ConfigServerInstanceMonitor.class), properties, + provider); } } diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/DiscoveryClientConfigDataConfigurationTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/DiscoveryClientConfigDataConfigurationTests.java new file mode 100644 index 00000000..0bb8694e --- /dev/null +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/DiscoveryClientConfigDataConfigurationTests.java @@ -0,0 +1,295 @@ +/* + * Copyright 2013-2019 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 org.springframework.cloud.config.client; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.Bootstrapper; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.event.HeartbeatEvent; +import org.springframework.cloud.config.client.ConfigClientProperties.Credentials; +import org.springframework.context.ConfigurableApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.cloud.config.client.ConfigClientProperties.Discovery.DEFAULT_CONFIG_SERVER; + +/** + * @author Dave Syer + */ +public class DiscoveryClientConfigDataConfigurationTests { + + protected ConfigurableApplicationContext context; + + protected DiscoveryClient client = Mockito.mock(DiscoveryClient.class); + + protected ServiceInstance info = new DefaultServiceInstance("app:8877", "app", "foo", 8877, false); + + @AfterEach + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void offByDefault() { + context = new SpringApplicationBuilder(TestConfig.class) + .addBootstrapper(registry -> registry.addCloseListener(event -> { + try { + event.getBootstrapContext().get(ConfigServerInstanceMonitor.class); + fail("ConfigServerInstanceMonitor was created when it shouldn't"); + } + catch (IllegalStateException e) { + // expected + } + })).run(); + } + + @Test + public void onWhenRequested() { + givenDiscoveryClientReturnsInfo(); + + setupAndRun(); + + verifyDiscoveryClientCalledOnce(); + expectConfigClientPropertiesHasConfigurationFromDiscovery(); + + } + + @Test + public void onWhenHeartbeat() { + setupAndRun(); + + givenDiscoveryClientReturnsInfo(); + verifyDiscoveryClientCalledOnce(); + + this.context.publishEvent(new HeartbeatEvent(this.context, "new")); + + expectConfigClientPropertiesHasConfigurationFromDiscovery(); + } + + @Test + public void secureWhenRequested() { + this.info = new DefaultServiceInstance("app:443", "app", "foo", 443, true); + givenDiscoveryClientReturnsInfo(); + + setupAndRun(); + + verifyDiscoveryClientCalledOnce(); + expectConfigClientPropertiesHasConfiguration("https://foo:443/"); + } + + @Test + public void multipleInstancesReturnedFromDiscovery() { + ServiceInstance info1 = new DefaultServiceInstance("app1:8888", "app", "localhost", 8888, true); + ServiceInstance info2 = new DefaultServiceInstance("app2:8888", "app", "localhost1", 8888, false); + givenDiscoveryClientReturnsInfo(info1, info2); + + setupAndRun(); + + verifyDiscoveryClientCalledOnce(); + + ConfigClientProperties properties = this.context.getBean(ConfigClientProperties.class); + assertThat(properties.getUri().length).isEqualTo(2); + Credentials credentials1 = properties.getCredentials(0); + Credentials credentials2 = properties.getCredentials(1); + assertThat(credentials1.getUri()).isEqualTo("https://localhost:8888/"); + assertThat(credentials2.getUri()).isEqualTo("http://localhost1:8888/"); + } + + @Test + public void setsPasssword() { + this.info.getMetadata().put("password", "bar"); + givenDiscoveryClientReturnsInfo(); + + setupAndRun(); + + ConfigClientProperties locator = this.context.getBean(ConfigClientProperties.class); + Credentials credentials = locator.getCredentials(0); + assertThat(credentials.getUri()).isEqualTo("http://foo:8877/"); + assertThat(credentials.getPassword()).isEqualTo("bar"); + assertThat(credentials.getUsername()).isEqualTo("user"); + } + + @Test + public void setsPath() { + this.info.getMetadata().put("configPath", "/bar"); + givenDiscoveryClientReturnsInfo(); + + setupAndRun(); + + expectConfigClientPropertiesHasConfiguration("http://foo:8877/bar"); + } + + @Test + public void shouldFailGetConfigServerInstanceFromDiscoveryClient() { + givenDiscoveryClientReturnsNoInfo(); + + setupAndRun(); + + verifyDiscoveryClientCalledOnce(); + expectConfigClientPropertiesHasDefaultConfiguration(); + } + + @Test + @Disabled + public void shouldRetryAndSucceedGetConfigServerInstanceFromDiscoveryClient() { + givenDiscoveryClientReturnsInfoOnThirdTry(); + + context = setup("spring.cloud.config.retry.maxAttempts=3", "spring.cloud.config.retry.initialInterval=10", + "spring.cloud.config.fail-fast=true").run(); + + verifyDiscoveryClientCalledThreeTimes(); + + this.context.publishEvent(new HeartbeatEvent(this.context, "new")); + + expectConfigClientPropertiesHasConfigurationFromDiscovery(); + } + + @Test + @Disabled + public void shouldNotRetryIfNotFailFastPropertySet() { + givenDiscoveryClientReturnsInfoOnThirdTry(); + + context = setup("spring.cloud.config.retry.maxAttempts=3", "spring.cloud.config.retry.initialInterval=10") + .run(); + + verifyDiscoveryClientCalledOnce(); + expectConfigClientPropertiesHasDefaultConfiguration(); + } + + @Test + @Disabled + public void shouldRetryAndFailWithExceptionGetConfigServerInstanceFromDiscoveryClient() { + givenDiscoveryClientReturnsNoInfo(); + + // expectNoInstancesOfConfigServerException(); + + context = setup("spring.cloud.config.retry.maxAttempts=3", "spring.cloud.config.retry.initialInterval=10", + "spring.cloud.config.fail-fast=true").run(); + } + + @Test + @Disabled + public void shouldRetryAndFailWithMessageGetConfigServerInstanceFromDiscoveryClient() { + givenDiscoveryClientReturnsNoInfo(); + + context = setup("spring.cloud.config.retry.maxAttempts=3", "spring.cloud.config.retry.initialInterval=10", + "spring.cloud.config.fail-fast=false").run(); + + expectConfigClientPropertiesHasDefaultConfiguration(); + } + + SpringApplicationBuilder setup(String... env) { + return setup(true, env); + } + + SpringApplicationBuilder setup(boolean addInstanceProvider, String... env) { + SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class) + .properties(addDefaultEnv(env)); + if (addInstanceProvider) { + builder.addBootstrapper(instanceProviderBootstrapper()); + } + return builder.addBootstrapper(registry -> registry.addCloseListener(event -> { + ConfigServerInstanceMonitor monitor = event.getBootstrapContext().get(ConfigServerInstanceMonitor.class); + assertThat(monitor).as("ConfigServerInstanceMonitor was not created when it should").isNotNull(); + })); + } + + protected Bootstrapper instanceProviderBootstrapper() { + return registry -> registry.register(ConfigServerInstanceProvider.Function.class, + BootstrapRegistry.InstanceSupplier.from(() -> this.client::getInstances)); + } + + protected void setupAndRun(String... env) { + context = setup(addDefaultEnv(env)).run(); + } + + private String[] addDefaultEnv(String[] env) { + Set set = new LinkedHashSet<>(); + if (env != null && env.length > 0) { + set.addAll(Arrays.asList(env)); + } + set.add("spring.cloud.config.discovery.enabled=true"); + set.add("spring.config.import=optional:configserver:"); + return set.toArray(new String[0]); + } + + void givenDiscoveryClientReturnsInfo() { + givenDiscoveryClientReturnsInfo(this.info); + } + + void givenDiscoveryClientReturnsInfo(ServiceInstance... instances) { + given(this.client.getInstances(DEFAULT_CONFIG_SERVER)).willReturn(Arrays.asList(instances)); + } + + void givenDiscoveryClientReturnsNoInfo() { + given(this.client.getInstances(DEFAULT_CONFIG_SERVER)).willReturn(Collections.emptyList()); + } + + void givenDiscoveryClientReturnsInfoOnThirdTry() { + given(this.client.getInstances(DEFAULT_CONFIG_SERVER)).willReturn(Collections.emptyList()) + .willReturn(Collections.emptyList()).willReturn(Collections.singletonList(this.info)); + } + + void verifyDiscoveryClientCalledOnce() { + verify(this.client).getInstances(DEFAULT_CONFIG_SERVER); + } + + void verifyDiscoveryClientCalledThreeTimes() { + verify(this.client, times(3)).getInstances(DEFAULT_CONFIG_SERVER); + } + + void expectConfigClientPropertiesHasConfigurationFromDiscovery() { + expectConfigClientPropertiesHasConfiguration("http://foo:8877/"); + } + + void expectConfigClientPropertiesHasDefaultConfiguration() { + expectConfigClientPropertiesHasConfiguration("http://localhost:8888"); + } + + void expectConfigClientPropertiesHasConfiguration(final String expectedUri) { + ConfigClientProperties properties = this.context.getBean(ConfigClientProperties.class); + Credentials credentials = properties.getCredentials(0); + assertThat(credentials.getUri()).isEqualTo(expectedUri); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + static class TestConfig { + + } + +}