Uses http to look up config server from eureka (#3796)

This eliminates the oddness of having multiple discovery client beans and related behavior and simplifies bootstrap significantly.

Moves to ConfigServerInstanceProvider.Function for config client bootstrap.

Removes conditions for dealing with DiscoverClient in parent context.

Updates tests to reflect the new reality of no DiscoveryClient in bootstrap.

Renames config bootstrap to EurekaConfigServerBootstrapConfiguration

Fixes gh-3795

See https://github.com/spring-cloud/spring-cloud-gateway/issues/1514
This commit is contained in:
Spencer Gibb
2020-05-14 15:04:30 -04:00
committed by GitHub
parent 182951a0d5
commit 8d19948f4b
10 changed files with 277 additions and 235 deletions

View File

@@ -113,13 +113,7 @@ public class EurekaClientAutoConfiguration {
@ConditionalOnMissingBean(value = EurekaClientConfig.class,
search = SearchStrategy.CURRENT)
public EurekaClientConfigBean eurekaClientConfigBean(ConfigurableEnvironment env) {
EurekaClientConfigBean client = new EurekaClientConfigBean();
if ("bootstrap".equals(this.env.getProperty("spring.config.name"))) {
// We don't register during bootstrap by default, but there will be another
// chance later.
client.setRegisterWithEureka(false);
}
return client;
return new EurekaClientConfigBean();
}
@Bean

View File

@@ -18,11 +18,13 @@ package org.springframework.cloud.netflix.eureka;
import java.net.URI;
import java.util.Map;
import java.util.Objects;
import com.netflix.appinfo.InstanceInfo;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import static com.netflix.appinfo.InstanceInfo.PortType.SECURE;
@@ -92,4 +94,27 @@ public class EurekaServiceInstance implements ServiceInstance {
return getUri().getScheme();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
EurekaServiceInstance that = (EurekaServiceInstance) o;
return Objects.equals(this.instance, that.instance);
}
@Override
public int hashCode() {
return Objects.hash(this.instance);
}
@Override
public String toString() {
return new ToStringCreator(this).append("instance", instance).toString();
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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.netflix.eureka.config;
import java.util.ArrayList;
import java.util.List;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClientConfig;
import com.netflix.discovery.endpoint.EndpointUtils;
import com.netflix.discovery.shared.Applications;
import com.netflix.discovery.shared.resolver.DefaultEndpoint;
import com.netflix.discovery.shared.transport.EurekaHttpResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.autoconfigure.condition.SearchStrategy;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.config.client.ConfigServerInstanceProvider;
import org.springframework.cloud.config.client.ConfigServicePropertySourceLocator;
import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean;
import org.springframework.cloud.netflix.eureka.EurekaServiceInstance;
import org.springframework.cloud.netflix.eureka.http.WebClientEurekaHttpClient;
import org.springframework.cloud.netflix.eureka.http.WebClientTransportClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
/**
* Bootstrap configuration for config client that wants to lookup the config server via
* discovery.
*
* @author Dave Syer
*/
@ConditionalOnClass(ConfigServicePropertySourceLocator.class)
@ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled",
matchIfMissing = false)
@Configuration(proxyBeanMethods = false)
public class EurekaConfigServerBootstrapConfiguration {
private static final Log log = LogFactory
.getLog(EurekaConfigServerBootstrapConfiguration.class);
@Bean
@ConditionalOnMissingBean(value = EurekaClientConfig.class,
search = SearchStrategy.CURRENT)
public EurekaClientConfigBean eurekaClientConfigBean() {
return new EurekaClientConfigBean();
}
@Bean
@ConditionalOnMissingBean
public WebClientEurekaHttpClient configDiscoveryEurekaHttpClient(
EurekaClientConfigBean config) {
List<String> urls = EndpointUtils.getServiceUrlsFromConfig(config,
EurekaClientConfigBean.DEFAULT_ZONE, true);
String url = urls.get(0);
return (WebClientEurekaHttpClient) new WebClientTransportClientFactory()
.newClient(new DefaultEndpoint(url));
}
private boolean isSuccessful(EurekaHttpResponse<Applications> response) {
HttpStatus httpStatus = HttpStatus.resolve(response.getStatusCode());
return httpStatus != null && httpStatus.is2xxSuccessful();
}
@Bean
public ConfigServerInstanceProvider.Function eurekaConfigServerInstanceProvider(
WebClientEurekaHttpClient client, EurekaClientConfig config) {
return serviceId -> {
if (log.isDebugEnabled()) {
log.debug("eurekaConfigServerInstanceProvider finding instances for "
+ serviceId);
}
EurekaHttpResponse<Applications> response = client
.getApplications(config.getRegion());
List<ServiceInstance> instances = new ArrayList<>();
if (!isSuccessful(response) || response.getEntity() == null) {
return instances;
}
Applications applications = response.getEntity();
applications.shuffleInstances(config.shouldFilterOnlyUpInstances());
List<InstanceInfo> infos = applications
.getInstancesByVirtualHostName(serviceId);
for (InstanceInfo info : infos) {
instances.add(new EurekaServiceInstance(info));
}
if (log.isDebugEnabled()) {
log.debug("eurekaConfigServerInstanceProvider found " + infos.size()
+ " instance(s) for " + serviceId + ", " + instances);
}
return instances;
};
}
}

View File

@@ -1,56 +0,0 @@
/*
* 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.netflix.eureka.config;
import javax.annotation.PostConstruct;
import com.netflix.discovery.EurekaClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
/**
* Bootstrap configuration for a config client that wants to lookup the config server via
* discovery.
*
* @author Dave Syer
*/
@ConditionalOnBean({ EurekaDiscoveryClientConfiguration.class })
@ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled",
matchIfMissing = false)
public class EurekaDiscoveryClientConfigServiceAutoConfiguration {
@Autowired
private ConfigurableApplicationContext context;
@PostConstruct
public void init() {
if (this.context.getParent() != null) {
if (this.context.getBeanNamesForType(EurekaClient.class).length > 0
&& this.context.getParent()
.getBeanNamesForType(EurekaClient.class).length > 0) {
// If the parent has a EurekaClient as well it should be shutdown, so the
// local one can register accurate instance info
this.context.getParent().getBean(EurekaClient.class).shutdown();
}
}
}
}

View File

@@ -1,48 +0,0 @@
/*
* 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.netflix.eureka.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration;
import org.springframework.cloud.config.client.ConfigServicePropertySourceLocator;
import org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration;
import org.springframework.cloud.netflix.eureka.reactive.EurekaReactiveDiscoveryClientConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* Eureka-specific helper for config client that wants to lookup the config server via
* discovery.
*
* @author Dave Syer
*/
@ConditionalOnClass(ConfigServicePropertySourceLocator.class)
@ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled",
matchIfMissing = false)
@Configuration(proxyBeanMethods = false)
@Import({ EurekaDiscoveryClientConfiguration.class, // this emulates
// @EnableDiscoveryClient, the import
// selector doesn't run before the
// bootstrap phase
EurekaClientAutoConfiguration.class,
EurekaReactiveDiscoveryClientConfiguration.class,
ReactiveCommonsClientAutoConfiguration.class })
public class EurekaDiscoveryClientConfigServiceBootstrapConfiguration {
}

View File

@@ -1,6 +1,5 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration,\
@@ -8,4 +7,4 @@ org.springframework.cloud.netflix.eureka.reactive.EurekaReactiveDiscoveryClientC
org.springframework.cloud.netflix.eureka.loadbalancer.LoadBalancerEurekaAutoConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration
org.springframework.cloud.netflix.eureka.config.EurekaConfigServerBootstrapConfiguration

View File

@@ -48,12 +48,10 @@ public class ConfigRefreshTests {
@Test
// This test is used to verify that getApplications is called the correct number of
// times
// when a refresh event is fired. The getApplications call in
// times when a refresh event is fired. The getApplications call in
// EurekaClientConfigurationRefresher.onApplicationEvent
// ensures that the EurekaClient bean is recreated after a refresh event and that we
// reregister the client with
// the server
// reregister the client with the server
public void verifyGetApplications() {
if (publisher != null) {
publisher.publishEvent(new RefreshScopeRefreshedEvent());

View File

@@ -1,114 +0,0 @@
/*
* 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.netflix.eureka.config;
import java.util.Arrays;
import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClient;
import org.junit.After;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.cloud.commons.util.UtilAutoConfiguration;
import org.springframework.cloud.config.client.ConfigClientProperties;
import org.springframework.cloud.config.client.DiscoveryClientConfigServiceBootstrapConfiguration;
import org.springframework.cloud.netflix.eureka.CloudEurekaClient;
import org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.times;
import static org.springframework.cloud.config.client.ConfigClientProperties.Discovery.DEFAULT_CONFIG_SERVER;
/**
* @author Dave Syer
*/
public class DiscoveryClientConfigServiceAutoConfigurationTests {
private AnnotationConfigApplicationContext context;
@After
public void close() {
if (this.context != null) {
if (this.context.getParent() != null) {
((AnnotationConfigApplicationContext) this.context.getParent()).close();
}
this.context.close();
}
}
@Test
public void onWhenRequested() throws Exception {
setup("spring.cloud.config.discovery.enabled=true",
"eureka.instance.metadataMap.foo:bar",
"eureka.instance.nonSecurePort:7001", "eureka.instance.hostname:foo");
assertThat(this.context.getBeanNamesForType(
EurekaDiscoveryClientConfigServiceAutoConfiguration.class).length)
.isEqualTo(1);
EurekaClient eurekaClient = this.context.getParent().getBean(EurekaClient.class);
Mockito.verify(eurekaClient, times(2))
.getInstancesByVipAddress(DEFAULT_CONFIG_SERVER, false);
Mockito.verify(eurekaClient, times(1)).shutdown();
ConfigClientProperties locator = this.context
.getBean(ConfigClientProperties.class);
assertThat(locator.getUri()[0]).isEqualTo("http://foo:7001/");
ApplicationInfoManager infoManager = this.context
.getBean(ApplicationInfoManager.class);
assertThat(infoManager.getInfo().getMetadata().get("foo")).isEqualTo("bar");
}
private void setup(String... env) {
AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
TestPropertyValues.of(env).applyTo(parent);
parent.register(UtilAutoConfiguration.class,
EurekaDiscoveryClientConfiguration.class,
PropertyPlaceholderAutoConfiguration.class, EnvironmentKnobbler.class,
EurekaDiscoveryClientConfigServiceBootstrapConfiguration.class,
DiscoveryClientConfigServiceBootstrapConfiguration.class,
ConfigClientProperties.class);
parent.refresh();
this.context = new AnnotationConfigApplicationContext();
this.context.setParent(parent);
this.context.register(PropertyPlaceholderAutoConfiguration.class,
EurekaDiscoveryClientConfigServiceAutoConfiguration.class,
EurekaClientAutoConfiguration.class);
this.context.refresh();
}
@Configuration(proxyBeanMethods = false)
protected static class EnvironmentKnobbler {
@Bean
public EurekaClient eurekaClient(ApplicationInfoManager manager) {
InstanceInfo info = manager.getInfo();
EurekaClient client = Mockito.mock(CloudEurekaClient.class);
given(client.getInstancesByVipAddress(DEFAULT_CONFIG_SERVER, false))
.willReturn(Arrays.asList(info));
return client;
}
}
}

View File

@@ -0,0 +1,131 @@
/*
* 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.netflix.eureka.config;
import java.util.Collections;
import java.util.List;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClient;
import com.netflix.discovery.shared.Applications;
import com.netflix.discovery.shared.transport.EurekaHttpResponse;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.system.OutputCaptureRule;
import org.springframework.cloud.config.client.ConfigServerInstanceProvider;
import org.springframework.cloud.netflix.eureka.CloudEurekaClient;
import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean;
import org.springframework.cloud.netflix.eureka.http.WebClientEurekaHttpClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Spencer Gibb
*/
public class EurekaConfigServerBootstrapConfigurationTests {
@Rule
public OutputCaptureRule output = new OutputCaptureRule();
@Test
public void offByDefault() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations
.of(EurekaConfigServerBootstrapConfiguration.class))
.run(context -> {
assertThat(context).doesNotHaveBean(EurekaClientConfigBean.class);
assertThat(context).doesNotHaveBean(WebClientEurekaHttpClient.class);
assertThat(context)
.doesNotHaveBean(ConfigServerInstanceProvider.Function.class);
});
}
@Test
public void properBeansCreatedWhenEnabled() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations
.of(EurekaConfigServerBootstrapConfiguration.class))
.withPropertyValues("spring.cloud.config.discovery.enabled=true")
.run(context -> {
assertThat(context).hasSingleBean(EurekaClientConfigBean.class);
assertThat(context).hasSingleBean(WebClientEurekaHttpClient.class);
assertThat(context)
.hasSingleBean(ConfigServerInstanceProvider.Function.class);
});
}
@Test
public void eurekaConfigServerInstanceProviderCalled() {
new SpringApplicationBuilder(TestConfigDiscoveryConfiguration.class).properties(
"spring.cloud.config.discovery.enabled=true",
"spring.main.sources="
+ TestConfigDiscoveryBootstrapConfiguration.class.getName(),
"logging.level.org.springframework.cloud.netflix.eureka.config=DEBUG")
.run();
assertThat(output).contains(
"eurekaConfigServerInstanceProvider finding instances for configserver")
.contains(
"eurekaConfigServerInstanceProvider found 1 instance(s) for configserver");
}
@SpringBootConfiguration
@EnableAutoConfiguration
protected static class TestConfigDiscoveryConfiguration {
@Bean
public EurekaClient getClient() {
return mock(CloudEurekaClient.class);
}
}
@Configuration
protected static class TestConfigDiscoveryBootstrapConfiguration {
@SuppressWarnings("unchecked")
@Bean
public WebClientEurekaHttpClient mockWebClientEurekaHttpClient() {
InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder()
.setAppName("configserver").build();
List<InstanceInfo> instanceInfos = Collections.singletonList(instanceInfo);
Applications applications = mock(Applications.class);
when(applications.getInstancesByVirtualHostName("configserver"))
.thenReturn(instanceInfos);
EurekaHttpResponse<Applications> response = mock(EurekaHttpResponse.class);
when(response.getStatusCode()).thenReturn(200);
when(response.getEntity()).thenReturn(applications);
WebClientEurekaHttpClient client = mock(WebClientEurekaHttpClient.class);
when(client.getApplications("us-east-1")).thenReturn(response);
return client;
}
}
}

View File

@@ -18,11 +18,10 @@ package org.springframework.cloud.netflix.eureka.sample;
import com.netflix.discovery.EurekaClient;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.CloudEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import static org.mockito.Mockito.mock;
@@ -30,8 +29,7 @@ import static org.mockito.Mockito.mock;
/**
* @author Ryan Baxter
*/
@Configuration(proxyBeanMethods = false)
@ComponentScan
@SpringBootConfiguration(proxyBeanMethods = false)
@EnableAutoConfiguration
@RestController
public class RefreshEurekaSampleApplication {