Adds ConfigData support for s.c.config.discovery.enabled=true

This commit is contained in:
spencergibb
2020-09-21 16:55:55 -04:00
parent ba53b6a551
commit 965d1cb168
6 changed files with 281 additions and 85 deletions

View File

@@ -16,18 +16,8 @@
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.EurekaHttpClient;
import com.netflix.discovery.shared.transport.EurekaHttpResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
@@ -38,11 +28,9 @@ import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
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.RestTemplateEurekaHttpClient;
import org.springframework.cloud.netflix.eureka.http.RestTemplateTransportClientFactory;
import org.springframework.cloud.netflix.eureka.http.WebClientEurekaHttpClient;
@@ -50,8 +38,6 @@ import org.springframework.cloud.netflix.eureka.http.WebClientTransportClientFac
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
/**
@@ -61,13 +47,11 @@ import org.springframework.web.reactive.function.client.WebClient;
* @author Dave Syer
*/
@ConditionalOnClass(ConfigServicePropertySourceLocator.class)
@ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled", matchIfMissing = false)
@ConditionalOnProperty("spring.cloud.config.discovery.enabled")
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
public class EurekaConfigServerBootstrapConfiguration {
private static final Log log = LogFactory.getLog(EurekaConfigServerBootstrapConfiguration.class);
@Bean
@ConditionalOnMissingBean(value = EurekaClientConfig.class, search = SearchStrategy.CURRENT)
public EurekaClientConfigBean eurekaClientConfigBean() {
@@ -81,78 +65,13 @@ public class EurekaConfigServerBootstrapConfiguration {
public RestTemplateEurekaHttpClient configDiscoveryRestTemplateEurekaHttpClient(EurekaClientConfigBean config,
Environment env) {
return (RestTemplateEurekaHttpClient) new RestTemplateTransportClientFactory()
.newClient(new DefaultEndpoint(getEurekaUrl(config, env)));
}
private static String getEurekaUrl(EurekaClientConfigBean config, Environment env) {
List<String> urls = EndpointUtils.getDiscoveryServiceUrls(config, EurekaClientConfigBean.DEFAULT_ZONE,
new HostnameBasedUrlRandomizer(env.getProperty("eureka.instance.hostname")));
return urls.get(0);
}
private boolean isSuccessful(EurekaHttpResponse<Applications> response) {
HttpStatus httpStatus = HttpStatus.resolve(response.getStatusCode());
return httpStatus != null && httpStatus.is2xxSuccessful();
.newClient(HostnameBasedUrlRandomizer.randomEndpoint(config, env));
}
@Bean
public ConfigServerInstanceProvider.Function eurekaConfigServerInstanceProvider(EurekaHttpClient 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;
};
}
private static final class HostnameBasedUrlRandomizer implements EndpointUtils.ServiceUrlRandomizer {
private final String hostname;
private HostnameBasedUrlRandomizer(String hostname) {
this.hostname = hostname;
}
@Override
public void randomize(List<String> urlList) {
int listSize = 0;
if (urlList != null) {
listSize = urlList.size();
}
if (!StringUtils.hasText(hostname) || listSize == 0) {
return;
}
// Find the hashcode of the instance hostname and use it to find an entry
// and then arrange the rest of the entries after this entry.
int instanceHashcode = hostname.hashCode();
if (instanceHashcode < 0) {
instanceHashcode = instanceHashcode * -1;
}
int backupInstance = instanceHashcode % listSize;
for (int i = 0; i < backupInstance; i++) {
String zone = urlList.remove(0);
urlList.add(zone);
}
}
return new EurekaConfigServerInstanceProvider(client, config)::getInstances;
}
@Configuration(proxyBeanMethods = false)
@@ -166,7 +85,7 @@ public class EurekaConfigServerBootstrapConfiguration {
public WebClientEurekaHttpClient configDiscoveryWebClientEurekaHttpClient(EurekaClientConfigBean config,
ObjectProvider<WebClient.Builder> builder, Environment env) {
return (WebClientEurekaHttpClient) new WebClientTransportClientFactory(builder::getIfAvailable)
.newClient(new DefaultEndpoint(getEurekaUrl(config, env)));
.newClient(HostnameBasedUrlRandomizer.randomEndpoint(config, env));
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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 com.netflix.discovery.shared.transport.EurekaHttpClient;
import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.Bootstrapper;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cloud.config.client.ConfigClientProperties;
import org.springframework.cloud.config.client.ConfigServerInstanceProvider;
import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean;
import org.springframework.cloud.netflix.eureka.http.RestTemplateTransportClientFactory;
public class EurekaConfigServerBootstrapper implements Bootstrapper {
@Override
public void intitialize(BootstrapRegistry registry) {
registry.registerIfAbsent(EurekaClientConfigBean.class, context -> {
Binder binder = context.get(Binder.class);
if (!getDiscoveryEnabled(binder)) {
return null;
}
return binder.bind(EurekaClientConfigBean.PREFIX, EurekaClientConfigBean.class)
.orElseGet(EurekaClientConfigBean::new);
});
registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, context -> {
Binder binder = context.get(Binder.class);
if (!getDiscoveryEnabled(binder)) {
return null;
}
EurekaClientConfigBean config = context.get(EurekaClientConfigBean.class);
EurekaHttpClient httpClient = new RestTemplateTransportClientFactory()
.newClient(HostnameBasedUrlRandomizer.randomEndpoint(config, binder));
return new EurekaConfigServerInstanceProvider(httpClient, config)::getInstances;
});
}
private Boolean getDiscoveryEnabled(Binder binder) {
return binder.bind(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED, Boolean.class).orElse(false);
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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.shared.Applications;
import com.netflix.discovery.shared.transport.EurekaHttpClient;
import com.netflix.discovery.shared.transport.EurekaHttpResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.netflix.eureka.EurekaServiceInstance;
import org.springframework.http.HttpStatus;
public class EurekaConfigServerInstanceProvider {
private final Log log;
private final EurekaHttpClient client;
private final EurekaClientConfig config;
public EurekaConfigServerInstanceProvider(EurekaHttpClient client, EurekaClientConfig config) {
this(LogFactory.getLog(EurekaConfigServerInstanceProvider.class), client, config);
}
public EurekaConfigServerInstanceProvider(Log log, EurekaHttpClient client, EurekaClientConfig config) {
this.log = log;
this.client = client;
this.config = config;
}
public List<ServiceInstance> getInstances(String 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;
}
private boolean isSuccessful(EurekaHttpResponse<Applications> response) {
HttpStatus httpStatus = HttpStatus.resolve(response.getStatusCode());
return httpStatus != null && httpStatus.is2xxSuccessful();
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.List;
import com.netflix.discovery.endpoint.EndpointUtils;
import com.netflix.discovery.shared.resolver.DefaultEndpoint;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
final class HostnameBasedUrlRandomizer implements EndpointUtils.ServiceUrlRandomizer {
private final String hostname;
HostnameBasedUrlRandomizer(String hostname) {
this.hostname = hostname;
}
@Override
public void randomize(List<String> urlList) {
int listSize = 0;
if (urlList != null) {
listSize = urlList.size();
}
if (!StringUtils.hasText(hostname) || listSize == 0) {
return;
}
// Find the hashcode of the instance hostname and use it to find an entry
// and then arrange the rest of the entries after this entry.
int instanceHashcode = hostname.hashCode();
if (instanceHashcode < 0) {
instanceHashcode = instanceHashcode * -1;
}
int backupInstance = instanceHashcode % listSize;
for (int i = 0; i < backupInstance; i++) {
String zone = urlList.remove(0);
urlList.add(zone);
}
}
static String getEurekaUrl(EurekaClientConfigBean config, String hostname) {
List<String> urls = EndpointUtils.getDiscoveryServiceUrls(config, EurekaClientConfigBean.DEFAULT_ZONE,
new HostnameBasedUrlRandomizer(hostname));
return urls.get(0);
}
static DefaultEndpoint randomEndpoint(EurekaClientConfigBean config, Environment env) {
String hostname = env.getProperty("eureka.instance.hostname");
return new DefaultEndpoint(getEurekaUrl(config, hostname));
}
static DefaultEndpoint randomEndpoint(EurekaClientConfigBean config, Binder binder) {
String hostname = binder.bind("eureka.instance.hostname", String.class).orElseGet(() -> null);
return new DefaultEndpoint(getEurekaUrl(config, hostname));
}
}

View File

@@ -8,3 +8,6 @@ org.springframework.cloud.netflix.eureka.loadbalancer.LoadBalancerEurekaAutoConf
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaConfigServerBootstrapConfiguration
org.springframework.boot.Bootstrapper=\
org.springframework.cloud.netflix.eureka.config.EurekaConfigServerBootstrapper

View File

@@ -0,0 +1,61 @@
/*
* 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.junit.jupiter.api.Test;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.config.client.ConfigServerInstanceProvider;
import static org.assertj.core.api.Assertions.assertThat;
public class EurekaConfigServerBootstrapperTests {
@Test
public void notEnabledDoesNotAddInstanceProviderFn() {
new SpringApplicationBuilder(TestConfig.class)
.properties("spring.cloud.service-registry.auto-registration.enabled=false")
.addBootstrapper(registry -> registry.addCloseListener(event -> {
ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext()
.get(ConfigServerInstanceProvider.Function.class);
assertThat(providerFn).as("ConfigServerInstanceProvider.Function was created when it shouldn't")
.isNull();
})).run().close();
}
@Test
public void enabledAddsInstanceProviderFn() {
new SpringApplicationBuilder(TestConfig.class)
.properties("spring.cloud.config.discovery.enabled=true",
"spring.cloud.service-registry.auto-registration.enabled=false")
.addBootstrapper(registry -> registry.addCloseListener(event -> {
ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext()
.get(ConfigServerInstanceProvider.Function.class);
assertThat(providerFn).as("ConfigServerInstanceProvider.Function was not created when it should.")
.isNotNull();
})).run().close();
}
@SpringBootConfiguration
@EnableAutoConfiguration
static class TestConfig {
}
}