Support looking up the config server via discoveryclient (#1354)
Fixes #1021 Co-authored-by: Ryan Baxter <524254+ryanjbaxter@users.noreply.github.com>
This commit is contained in:
@@ -38,6 +38,11 @@
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-config-client</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing Dependencies -->
|
||||
<dependency>
|
||||
@@ -55,11 +60,6 @@
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-config-client</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2019-2023 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.kubernetes.client.discovery;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.kubernetes.client.informer.SharedIndexInformer;
|
||||
import io.kubernetes.client.informer.SharedInformerFactory;
|
||||
import io.kubernetes.client.informer.cache.Lister;
|
||||
import io.kubernetes.client.openapi.ApiClient;
|
||||
import io.kubernetes.client.openapi.models.V1Endpoints;
|
||||
import io.kubernetes.client.openapi.models.V1EndpointsList;
|
||||
import io.kubernetes.client.openapi.models.V1Service;
|
||||
import io.kubernetes.client.openapi.models.V1ServiceList;
|
||||
import io.kubernetes.client.util.Namespaces;
|
||||
import io.kubernetes.client.util.generic.GenericKubernetesApi;
|
||||
import org.apache.commons.logging.Log;
|
||||
|
||||
import org.springframework.boot.BootstrapContext;
|
||||
import org.springframework.boot.BootstrapRegistry;
|
||||
import org.springframework.boot.context.properties.bind.BindHandler;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.config.client.ConfigServerInstanceProvider;
|
||||
import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper;
|
||||
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.core.env.AbstractEnvironment;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* @author Ryan Baxter
|
||||
*/
|
||||
class KubernetesClientConfigServerBootstrapper extends KubernetesConfigServerBootstrapper {
|
||||
|
||||
@Override
|
||||
public void initialize(BootstrapRegistry registry) {
|
||||
if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) {
|
||||
return;
|
||||
}
|
||||
// We need to pass a lambda here rather than create a new instance of
|
||||
// ConfigServerInstanceProvider.Function
|
||||
// or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on
|
||||
// the classpath
|
||||
registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create);
|
||||
}
|
||||
|
||||
final static class KubernetesFunction implements ConfigServerInstanceProvider.Function {
|
||||
|
||||
private final BootstrapContext context;
|
||||
|
||||
private KubernetesFunction(BootstrapContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
static KubernetesFunction create(BootstrapContext context) {
|
||||
return new KubernetesFunction(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ServiceInstance> apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) {
|
||||
if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) {
|
||||
// If we don't have the Binder or BinderHandler from the
|
||||
// ConfigDataLocationResolverContext
|
||||
// we won't be able to create the necessary configuration
|
||||
// properties to configure the
|
||||
// Kubernetes DiscoveryClient
|
||||
return Collections.emptyList();
|
||||
}
|
||||
KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder,
|
||||
bindHandler);
|
||||
KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler);
|
||||
return getInstanceProvider(discoveryProperties, clientProperties, context, binder, bindHandler, log)
|
||||
.getInstances(serviceId);
|
||||
}
|
||||
|
||||
protected KubernetesConfigServerInstanceProvider getInstanceProvider(
|
||||
KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties,
|
||||
BootstrapContext context, Binder binder, BindHandler bindHandler, Log log) {
|
||||
if (context.isRegistered(KubernetesInformerDiscoveryClient.class)) {
|
||||
KubernetesInformerDiscoveryClient client = context.get(KubernetesInformerDiscoveryClient.class);
|
||||
return client::getInstances;
|
||||
}
|
||||
else {
|
||||
KubernetesClientAutoConfiguration clientAutoConfiguration = new KubernetesClientAutoConfiguration();
|
||||
ApiClient apiClient = context.getOrElseSupply(ApiClient.class,
|
||||
() -> clientAutoConfiguration.apiClient(clientProperties));
|
||||
|
||||
KubernetesNamespaceProvider kubernetesNamespaceProvider = clientAutoConfiguration
|
||||
.kubernetesNamespaceProvider(getNamespaceEnvironment(binder, bindHandler));
|
||||
|
||||
String namespace = getInformerNamespace(kubernetesNamespaceProvider, discoveryProperties);
|
||||
SharedInformerFactory sharedInformerFactory = new SharedInformerFactory(apiClient);
|
||||
SpringCloudKubernetesInformerFactoryProcessor informerFactoryProcessor = new SpringCloudKubernetesInformerFactoryProcessor(
|
||||
kubernetesNamespaceProvider, apiClient, sharedInformerFactory,
|
||||
discoveryProperties.isAllNamespaces());
|
||||
final GenericKubernetesApi<V1Service, V1ServiceList> servicesApi = new GenericKubernetesApi<>(
|
||||
V1Service.class, V1ServiceList.class, "", "v1", "services", apiClient);
|
||||
SharedIndexInformer<V1Service> serviceSharedIndexInformer = sharedInformerFactory
|
||||
.sharedIndexInformerFor(servicesApi, V1Service.class, 0L, namespace);
|
||||
Lister<V1Service> serviceLister = new Lister<>(serviceSharedIndexInformer.getIndexer());
|
||||
final GenericKubernetesApi<V1Endpoints, V1EndpointsList> endpointsApi = new GenericKubernetesApi<>(
|
||||
V1Endpoints.class, V1EndpointsList.class, "", "v1", "endpoints", apiClient);
|
||||
SharedIndexInformer<V1Endpoints> endpointsSharedIndexInformer = sharedInformerFactory
|
||||
.sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0L, namespace);
|
||||
Lister<V1Endpoints> endpointsLister = new Lister<>(endpointsSharedIndexInformer.getIndexer());
|
||||
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(
|
||||
kubernetesNamespaceProvider.getNamespace(), sharedInformerFactory, serviceLister,
|
||||
endpointsLister, serviceSharedIndexInformer, endpointsSharedIndexInformer, discoveryProperties);
|
||||
try {
|
||||
discoveryClient.afterPropertiesSet();
|
||||
return discoveryClient::getInstances;
|
||||
}
|
||||
catch (Exception e) {
|
||||
if (log != null) {
|
||||
log.warn("Error initiating informer discovery client", e);
|
||||
}
|
||||
return (serviceId) -> Collections.emptyList();
|
||||
}
|
||||
finally {
|
||||
sharedInformerFactory.stopAllRegisteredInformers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getInformerNamespace(KubernetesNamespaceProvider kubernetesNamespaceProvider,
|
||||
KubernetesDiscoveryProperties discoveryProperties) {
|
||||
return discoveryProperties.isAllNamespaces() ? Namespaces.NAMESPACE_ALL
|
||||
: kubernetesNamespaceProvider.getNamespace() == null ? Namespaces.NAMESPACE_DEFAULT
|
||||
: kubernetesNamespaceProvider.getNamespace();
|
||||
}
|
||||
|
||||
private Environment getNamespaceEnvironment(Binder binder, BindHandler bindHandler) {
|
||||
return new AbstractEnvironment() {
|
||||
@Override
|
||||
public String getProperty(String key) {
|
||||
return binder.bind(key, Bindable.of(String.class), bindHandler).orElse(super.getProperty(key));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// This method should never be called, but is there for backward
|
||||
// compatibility purposes
|
||||
@Override
|
||||
public List<ServiceInstance> apply(String serviceId) {
|
||||
return apply(serviceId, null, null, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,3 +5,6 @@ org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInforme
|
||||
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
|
||||
org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientConfigClientBootstrapConfiguration
|
||||
|
||||
org.springframework.boot.BootstrapRegistryInitializer=\
|
||||
org.springframework.cloud.kubernetes.client.discovery.KubernetesClientConfigServerBootstrapper
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2019-2023 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.kubernetes.client.discovery;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.client.WireMock;
|
||||
import io.kubernetes.client.openapi.ApiClient;
|
||||
import io.kubernetes.client.openapi.JSON;
|
||||
import io.kubernetes.client.openapi.models.V1EndpointAddress;
|
||||
import io.kubernetes.client.openapi.models.V1EndpointPort;
|
||||
import io.kubernetes.client.openapi.models.V1EndpointSubset;
|
||||
import io.kubernetes.client.openapi.models.V1Endpoints;
|
||||
import io.kubernetes.client.openapi.models.V1EndpointsList;
|
||||
import io.kubernetes.client.openapi.models.V1EndpointsListBuilder;
|
||||
import io.kubernetes.client.openapi.models.V1ListMetaBuilder;
|
||||
import io.kubernetes.client.openapi.models.V1ObjectMeta;
|
||||
import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder;
|
||||
import io.kubernetes.client.openapi.models.V1ObjectReferenceBuilder;
|
||||
import io.kubernetes.client.openapi.models.V1ServiceBuilder;
|
||||
import io.kubernetes.client.openapi.models.V1ServiceList;
|
||||
import io.kubernetes.client.openapi.models.V1ServiceListBuilder;
|
||||
import io.kubernetes.client.openapi.models.V1ServicePortBuilder;
|
||||
import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder;
|
||||
import io.kubernetes.client.util.ClientBuilder;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.environment.Environment;
|
||||
import org.springframework.cloud.config.environment.PropertySource;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
/**
|
||||
* @author Ryan Baxter
|
||||
*/
|
||||
class KubernetesClientConfigServerBootstrapperTests {
|
||||
|
||||
private static WireMockServer wireMockServer;
|
||||
|
||||
private ConfigurableApplicationContext context;
|
||||
|
||||
@BeforeEach
|
||||
public void before() throws JsonProcessingException {
|
||||
wireMockServer = new WireMockServer(options().dynamicPort());
|
||||
wireMockServer.start();
|
||||
WireMock.configureFor(wireMockServer.port());
|
||||
|
||||
V1ServiceList SERVICE_LIST = new V1ServiceListBuilder()
|
||||
.withMetadata(new V1ListMetaBuilder().withResourceVersion("1").build())
|
||||
.addToItems(new V1ServiceBuilder()
|
||||
.withMetadata(new V1ObjectMetaBuilder().withName("spring-cloud-kubernetes-configserver")
|
||||
.withNamespace("default").withResourceVersion("0").addToLabels("beta", "true")
|
||||
.addToAnnotations("org.springframework.cloud", "true").withUid("0").build())
|
||||
.withSpec(new V1ServiceSpecBuilder().withClusterIP("localhost").withSessionAffinity("None")
|
||||
.withType("ClusterIP")
|
||||
.addToPorts(new V1ServicePortBuilder().withPort(wireMockServer.port()).withName("http")
|
||||
.withProtocol("TCP").withNewTargetPort(wireMockServer.port()).build())
|
||||
.build())
|
||||
.build())
|
||||
.build();
|
||||
|
||||
V1EndpointsList ENDPOINTS_LIST = new V1EndpointsListBuilder()
|
||||
.withMetadata(new V1ListMetaBuilder().withResourceVersion("0").build())
|
||||
.addToItems(new V1Endpoints()
|
||||
.metadata(new V1ObjectMeta().name("spring-cloud-kubernetes-configserver").namespace("default"))
|
||||
.addSubsetsItem(
|
||||
new V1EndpointSubset()
|
||||
.addPortsItem(new V1EndpointPort().port(wireMockServer.port()).name("http"))
|
||||
.addAddressesItem(new V1EndpointAddress().hostname("localhost").ip("localhost")
|
||||
.targetRef(new V1ObjectReferenceBuilder().withUid("uid1").build()))))
|
||||
.build();
|
||||
|
||||
Environment environment = new Environment("test", "default");
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("hello", "world");
|
||||
org.springframework.cloud.config.environment.PropertySource p = new PropertySource("p1", properties);
|
||||
environment.add(p);
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
stubFor(get("/application/default")
|
||||
.willReturn(aResponse().withStatus(200).withBody(objectMapper.writeValueAsString(environment))
|
||||
.withHeader("content-type", "application/json")));
|
||||
stubFor(get("/api/v1/namespaces/default/endpoints?resourceVersion=0&watch=false")
|
||||
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(ENDPOINTS_LIST))
|
||||
.withHeader("content-type", "application/json")));
|
||||
stubFor(get("/api/v1/namespaces/default/services?resourceVersion=0&watch=false")
|
||||
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SERVICE_LIST))
|
||||
.withHeader("content-type", "application/json")));
|
||||
stubFor(get(urlMatching("/api/v1/namespaces/default/services.*.watch=true"))
|
||||
.willReturn(aResponse().withStatus(200)));
|
||||
stubFor(get(urlMatching("/api/v1/namespaces/default/endpoints.*.watch=true"))
|
||||
.willReturn(aResponse().withStatus(200)));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after() {
|
||||
wireMockServer.stop();
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBootstrapper() {
|
||||
this.context = setup().run();
|
||||
verify(getRequestedFor(urlEqualTo("/application/default")));
|
||||
assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world");
|
||||
}
|
||||
|
||||
SpringApplicationBuilder setup(String... env) {
|
||||
SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class)
|
||||
.properties(addDefaultEnv(env));
|
||||
ApiClient apiClient = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port())
|
||||
.setReadTimeout(Duration.ZERO).build();
|
||||
builder.addBootstrapRegistryInitializer(registry -> registry.register(ApiClient.class, (context) -> apiClient));
|
||||
builder.addBootstrapRegistryInitializer(new KubernetesClientConfigServerBootstrapper());
|
||||
return builder;
|
||||
}
|
||||
|
||||
private String[] addDefaultEnv(String[] env) {
|
||||
Set<String> 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:");
|
||||
set.add("spring.cloud.config.discovery.service-id=spring-cloud-kubernetes-configserver");
|
||||
set.add("spring.cloud.kubernetes.client.namespace=default");
|
||||
return set.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
public ApiClient apiClient() {
|
||||
return new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -67,6 +67,11 @@
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-config-client</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.kubernetes.commons.config;
|
||||
|
||||
import org.springframework.boot.BootstrapRegistryInitializer;
|
||||
import org.springframework.boot.context.properties.bind.BindHandler;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.cloud.config.client.ConfigClientProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* @author Ryan Baxter
|
||||
*/
|
||||
public abstract class KubernetesConfigServerBootstrapper implements BootstrapRegistryInitializer {
|
||||
|
||||
public static boolean hasConfigServerInstanceProvider() {
|
||||
return !ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null);
|
||||
}
|
||||
|
||||
public static KubernetesDiscoveryProperties createKubernetesDiscoveryProperties(Binder binder,
|
||||
BindHandler bindHandler) {
|
||||
return binder.bind("spring.cloud.kubernetes.discovery", Bindable.of(KubernetesDiscoveryProperties.class),
|
||||
bindHandler).orElseGet(KubernetesDiscoveryProperties::new);
|
||||
}
|
||||
|
||||
public static KubernetesClientProperties createKubernetesClientProperties(Binder binder, BindHandler bindHandler) {
|
||||
return binder.bind("spring.cloud.kubernetes.client", Bindable.of(KubernetesClientProperties.class), bindHandler)
|
||||
.orElseGet(KubernetesClientProperties::new);
|
||||
}
|
||||
|
||||
public static Boolean getDiscoveryEnabled(Binder binder, BindHandler bindHandler) {
|
||||
return binder.bind(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED, Bindable.of(Boolean.class), bindHandler)
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.kubernetes.commons.config;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
|
||||
/**
|
||||
* @author Ryan Baxter
|
||||
*/
|
||||
public interface KubernetesConfigServerInstanceProvider {
|
||||
|
||||
List<ServiceInstance> getInstances(String serviceId);
|
||||
|
||||
}
|
||||
@@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||
classes = KubernetesCommonsAutoConfigurationTests.App.class,
|
||||
properties = { "spring.cloud.kubernetes.client.password=mypassword",
|
||||
"spring.cloud.kubernetes.client.proxy-password=myproxypassword" })
|
||||
"spring.cloud.kubernetes.client.proxy-password=myproxypassword", "spring.cloud.config.enabled=false" })
|
||||
public class KubernetesCommonsAutoConfigurationTests {
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -42,7 +42,13 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*/
|
||||
public class KubernetesBootstrapConfigurationTests {
|
||||
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class)
|
||||
@SpringBootApplication
|
||||
static class App {
|
||||
|
||||
}
|
||||
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class,
|
||||
properties = { "spring.cloud.config.enabled=false" })
|
||||
@Nested
|
||||
public class FailFastDisabled {
|
||||
|
||||
@@ -57,7 +63,7 @@ public class KubernetesBootstrapConfigurationTests {
|
||||
}
|
||||
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class,
|
||||
properties = { "spring.cloud.kubernetes.config.fail-fast=true" })
|
||||
properties = { "spring.cloud.kubernetes.config.fail-fast=true", "spring.cloud.config.enabled=false" })
|
||||
@Nested
|
||||
public class ConfigFailFastEnabled {
|
||||
|
||||
@@ -89,7 +95,7 @@ public class KubernetesBootstrapConfigurationTests {
|
||||
}
|
||||
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class,
|
||||
properties = { "spring.cloud.kubernetes.secrets.fail-fast=true" })
|
||||
properties = { "spring.cloud.kubernetes.secrets.fail-fast=true", "spring.cloud.config.enabled=false" })
|
||||
@Nested
|
||||
public class SecretsFailFastEnabled {
|
||||
|
||||
@@ -120,8 +126,9 @@ public class KubernetesBootstrapConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class, properties = {
|
||||
"spring.cloud.kubernetes.config.fail-fast=true", "spring.cloud.kubernetes.secrets.fail-fast=true" })
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class,
|
||||
properties = { "spring.cloud.kubernetes.config.fail-fast=true",
|
||||
"spring.cloud.kubernetes.secrets.fail-fast=true", "spring.cloud.config.enabled=false" })
|
||||
@Nested
|
||||
public class ConfigAndSecretsFailFastEnabledWithDefaultRetryConfiguration {
|
||||
|
||||
@@ -170,7 +177,7 @@ public class KubernetesBootstrapConfigurationTests {
|
||||
"spring.cloud.kubernetes.config.retry.max-attempts=3",
|
||||
"spring.cloud.kubernetes.config.retry.initial-interval=1500",
|
||||
"spring.cloud.kubernetes.config.retry.max-interval=3000",
|
||||
"spring.cloud.kubernetes.config.retry.multiplier=1.5" })
|
||||
"spring.cloud.kubernetes.config.retry.multiplier=1.5", "spring.cloud.config.enabled=false" })
|
||||
@Nested
|
||||
public class ConfigFailFastEnabledWithCustomRetryConfiguration {
|
||||
|
||||
@@ -194,7 +201,7 @@ public class KubernetesBootstrapConfigurationTests {
|
||||
"spring.cloud.kubernetes.secrets.retry.max-attempts=3",
|
||||
"spring.cloud.kubernetes.secrets.retry.initial-interval=1500",
|
||||
"spring.cloud.kubernetes.secrets.retry.max-interval=3000",
|
||||
"spring.cloud.kubernetes.secrets.retry.multiplier=1.5" })
|
||||
"spring.cloud.kubernetes.secrets.retry.multiplier=1.5", "spring.cloud.config.enabled=false" })
|
||||
@Nested
|
||||
public class SecretsFailFastEnabledWithCustomRetryConfiguration {
|
||||
|
||||
@@ -213,8 +220,9 @@ public class KubernetesBootstrapConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class, properties = {
|
||||
"spring.cloud.kubernetes.config.fail-fast=true", "spring.cloud.kubernetes.config.retry.enabled=false" })
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class,
|
||||
properties = { "spring.cloud.kubernetes.config.fail-fast=true",
|
||||
"spring.cloud.kubernetes.config.retry.enabled=false", "spring.cloud.config.enabled=false" })
|
||||
@Nested
|
||||
public class ConfigFailFastEnabledButRetryDisabled {
|
||||
|
||||
@@ -228,8 +236,9 @@ public class KubernetesBootstrapConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class, properties = {
|
||||
"spring.cloud.kubernetes.secrets.fail-fast=true", "spring.cloud.kubernetes.secrets.retry.enabled=false" })
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class,
|
||||
properties = { "spring.cloud.kubernetes.secrets.fail-fast=true",
|
||||
"spring.cloud.kubernetes.secrets.retry.enabled=false", "spring.cloud.config.enabled=false" })
|
||||
@Nested
|
||||
public class SecretsFailFastEnabledButRetryDisabled {
|
||||
|
||||
@@ -272,9 +281,4 @@ public class KubernetesBootstrapConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@SpringBootApplication
|
||||
static class App {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,15 @@
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-commons</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-commons</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-config-client</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-actuator</artifactId>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.kubernetes.discovery;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
|
||||
import org.springframework.boot.BootstrapContext;
|
||||
import org.springframework.boot.BootstrapRegistry;
|
||||
import org.springframework.boot.context.properties.bind.BindHandler;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.config.client.ConfigServerInstanceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper;
|
||||
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* @author Ryan Baxter
|
||||
*/
|
||||
class ConfigServerBootstrapper extends KubernetesConfigServerBootstrapper {
|
||||
|
||||
@Override
|
||||
public void initialize(BootstrapRegistry registry) {
|
||||
if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) {
|
||||
return;
|
||||
}
|
||||
// We need to pass a lambda here rather than create a new instance of
|
||||
// ConfigServerInstanceProvider.Function
|
||||
// or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on
|
||||
// the classpath
|
||||
registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create);
|
||||
}
|
||||
|
||||
final static class KubernetesFunction implements ConfigServerInstanceProvider.Function {
|
||||
|
||||
private final BootstrapContext context;
|
||||
|
||||
private KubernetesFunction(BootstrapContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
static KubernetesFunction create(BootstrapContext context) {
|
||||
return new KubernetesFunction(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ServiceInstance> apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) {
|
||||
if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) {
|
||||
// If we don't have the Binder or BinderHandler from the
|
||||
// ConfigDataLocationResolverContext
|
||||
// we won't be able to create the necessary configuration
|
||||
// properties to configure the
|
||||
// Kubernetes DiscoveryClient
|
||||
return Collections.emptyList();
|
||||
}
|
||||
KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder,
|
||||
bindHandler);
|
||||
KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler);
|
||||
return getInstanceProvider(discoveryProperties, clientProperties, context, binder, bindHandler, log)
|
||||
.getInstances(serviceId);
|
||||
}
|
||||
|
||||
private KubernetesConfigServerInstanceProvider getInstanceProvider(
|
||||
KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties,
|
||||
BootstrapContext context, Binder binder, BindHandler bindHandler, Log log) {
|
||||
KubernetesDiscoveryClientProperties kubernetesDiscoveryClientProperties = binder
|
||||
.bind("spring.cloud.kubernetes.discovery", Bindable.of(KubernetesDiscoveryClientProperties.class),
|
||||
bindHandler)
|
||||
.orElseGet(KubernetesDiscoveryClientProperties::new);
|
||||
KubernetesDiscoveryClientAutoConfiguration.Servlet autoConfiguration = new KubernetesDiscoveryClientAutoConfiguration.Servlet();
|
||||
DiscoveryClient discoveryClient = autoConfiguration
|
||||
.kubernetesDiscoveryClient(autoConfiguration.restTemplate(), kubernetesDiscoveryClientProperties);
|
||||
return discoveryClient::getInstances;
|
||||
}
|
||||
|
||||
// This method should never be called, but is there for backward
|
||||
// compatibility purposes
|
||||
@Override
|
||||
public List<ServiceInstance> apply(String serviceId) {
|
||||
return apply(serviceId, null, null, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,2 +1,5 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClientAutoConfiguration
|
||||
|
||||
org.springframework.boot.BootstrapRegistryInitializer=\
|
||||
org.springframework.cloud.kubernetes.discovery.ConfigServerBootstrapper
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.kubernetes.discovery;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.client.WireMock;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.environment.Environment;
|
||||
import org.springframework.cloud.config.environment.PropertySource;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Ryan Baxter
|
||||
*/
|
||||
class ConfigServerBootstrapperTests {
|
||||
|
||||
private static WireMockServer wireMockServer;
|
||||
|
||||
protected ConfigurableApplicationContext context;
|
||||
|
||||
@AfterEach
|
||||
void close() {
|
||||
wireMockServer.stop();
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void beforeAll() throws JsonProcessingException {
|
||||
wireMockServer = new WireMockServer(options().dynamicPort());
|
||||
wireMockServer.start();
|
||||
WireMock.configureFor(wireMockServer.port());
|
||||
String APPS_NAME = "[{\"instanceId\":\"uid2\",\"serviceId\":\"spring-cloud-kubernetes-configserver\",\"host\":\"localhost\",\"port\":"
|
||||
+ wireMockServer.port() + ",\"uri\":\"" + wireMockServer.baseUrl()
|
||||
+ "\",\"secure\":false,\"metadata\":{\"spring\":\"true\",\"http\":\"8080\",\"k8s\":\"true\"},\"namespace\":\"namespace1\",\"cluster\":null,\"scheme\":\"http\"}]";
|
||||
stubFor(get("/apps/spring-cloud-kubernetes-configserver").willReturn(
|
||||
aResponse().withStatus(200).withBody(APPS_NAME).withHeader("content-type", "application/json")));
|
||||
Environment environment = new Environment("test", "default");
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("hello", "world");
|
||||
PropertySource p = new PropertySource("p1", properties);
|
||||
environment.add(p);
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
stubFor(get("/application/default")
|
||||
.willReturn(aResponse().withStatus(200).withBody(objectMapper.writeValueAsString(environment))
|
||||
.withHeader("content-type", "application/json")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBootstrapper() {
|
||||
this.context = setup().run();
|
||||
verify(1, getRequestedFor(urlEqualTo("/apps/spring-cloud-kubernetes-configserver")));
|
||||
assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world");
|
||||
}
|
||||
|
||||
SpringApplicationBuilder setup(String... env) {
|
||||
SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class)
|
||||
.properties(addDefaultEnv(env));
|
||||
builder.addBootstrapRegistryInitializer(new ConfigServerBootstrapper());
|
||||
return builder;
|
||||
}
|
||||
|
||||
private String[] addDefaultEnv(String[] env) {
|
||||
Set<String> 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:");
|
||||
set.add("spring.cloud.config.discovery.service-id=spring-cloud-kubernetes-configserver");
|
||||
set.add("spring.cloud.kubernetes.discovery.discoveryServerUrl=" + wireMockServer.baseUrl());
|
||||
return set.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -60,6 +60,11 @@
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-config-client</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing Dependencies -->
|
||||
<dependency>
|
||||
@@ -117,13 +122,13 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-config-client</artifactId>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<groupId>com.github.tomakehurst</groupId>
|
||||
<artifactId>wiremock-jre8</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2012-2023 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.kubernetes.fabric8.discovery;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.fabric8.kubernetes.client.Config;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import org.apache.commons.logging.Log;
|
||||
|
||||
import org.springframework.boot.BootstrapContext;
|
||||
import org.springframework.boot.BootstrapRegistry;
|
||||
import org.springframework.boot.context.properties.bind.BindHandler;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.config.client.ConfigServerInstanceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper;
|
||||
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.cloud.kubernetes.fabric8.Fabric8AutoConfiguration;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* @author Ryan Baxter
|
||||
*/
|
||||
class Fabric8ConfigServerBootstrapper extends KubernetesConfigServerBootstrapper {
|
||||
|
||||
@Override
|
||||
public void initialize(BootstrapRegistry registry) {
|
||||
if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) {
|
||||
return;
|
||||
}
|
||||
// We need to pass a lambda here rather than create a new instance of
|
||||
// ConfigServerInstanceProvider.Function
|
||||
// or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on
|
||||
// the classpath
|
||||
registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create);
|
||||
}
|
||||
|
||||
final static class KubernetesFunction implements ConfigServerInstanceProvider.Function {
|
||||
|
||||
private final BootstrapContext context;
|
||||
|
||||
private KubernetesFunction(BootstrapContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
static KubernetesFunction create(BootstrapContext context) {
|
||||
return new KubernetesFunction(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ServiceInstance> apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) {
|
||||
if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) {
|
||||
// If we don't have the Binder or BinderHandler from the
|
||||
// ConfigDataLocationResolverContext
|
||||
// we won't be able to create the necessary configuration
|
||||
// properties to configure the
|
||||
// Kubernetes DiscoveryClient
|
||||
return Collections.emptyList();
|
||||
}
|
||||
KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder,
|
||||
bindHandler);
|
||||
KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler);
|
||||
return getInstanceProvider(discoveryProperties, clientProperties, context).getInstances(serviceId);
|
||||
}
|
||||
|
||||
private KubernetesConfigServerInstanceProvider getInstanceProvider(
|
||||
KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties,
|
||||
BootstrapContext context) {
|
||||
if (context.isRegistered(KubernetesDiscoveryClient.class)) {
|
||||
KubernetesDiscoveryClient client = context.get(KubernetesDiscoveryClient.class);
|
||||
return client::getInstances;
|
||||
}
|
||||
else {
|
||||
Fabric8AutoConfiguration fabric8AutoConfiguration = new Fabric8AutoConfiguration();
|
||||
Config config = fabric8AutoConfiguration.kubernetesClientConfig(clientProperties);
|
||||
KubernetesClient kubernetesClient = fabric8AutoConfiguration.kubernetesClient(config);
|
||||
KubernetesDiscoveryClientAutoConfiguration discoveryClientAutoConfiguration = new KubernetesDiscoveryClientAutoConfiguration();
|
||||
KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(kubernetesClient,
|
||||
discoveryProperties, discoveryClientAutoConfiguration.servicesFunction(discoveryProperties),
|
||||
new ServicePortSecureResolver(discoveryProperties));
|
||||
return discoveryClient::getInstances;
|
||||
}
|
||||
}
|
||||
|
||||
// This method should never be called, but is there for backward
|
||||
// compatibility purposes
|
||||
@Override
|
||||
public List<ServiceInstance> apply(String serviceId) {
|
||||
return apply(serviceId, null, null, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,3 +4,5 @@ org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClient
|
||||
org.springframework.cloud.kubernetes.fabric8.discovery.reactive.KubernetesReactiveDiscoveryClientAutoConfiguration
|
||||
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
|
||||
org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClientConfigClientBootstrapConfiguration
|
||||
org.springframework.boot.BootstrapRegistryInitializer=\
|
||||
org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8ConfigServerBootstrapper
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2013-2023 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.kubernetes.fabric8.discovery;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||
import com.github.tomakehurst.wiremock.client.WireMock;
|
||||
import io.fabric8.kubernetes.api.model.Endpoints;
|
||||
import io.fabric8.kubernetes.api.model.EndpointsBuilder;
|
||||
import io.fabric8.kubernetes.api.model.Service;
|
||||
import io.fabric8.kubernetes.api.model.ServiceBuilder;
|
||||
import io.fabric8.kubernetes.client.Config;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.environment.Environment;
|
||||
import org.springframework.cloud.config.environment.PropertySource;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.get;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
|
||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
/**
|
||||
* @author Ryan Baxter
|
||||
*/
|
||||
@EnableKubernetesMockClient(crud = true, https = false)
|
||||
class Fabric8ConfigServerBootstrapperTests {
|
||||
|
||||
private static WireMockServer wireMockServer;
|
||||
|
||||
private KubernetesClient mockClient;
|
||||
|
||||
private ConfigurableApplicationContext context;
|
||||
|
||||
@BeforeEach
|
||||
public void before() throws JsonProcessingException {
|
||||
wireMockServer = new WireMockServer(options().dynamicPort());
|
||||
wireMockServer.start();
|
||||
WireMock.configureFor(wireMockServer.port());
|
||||
|
||||
// Configure the kubernetes master url to point to the mock server
|
||||
System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl());
|
||||
System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true");
|
||||
System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");
|
||||
System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false");
|
||||
System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true");
|
||||
|
||||
Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("spring-cloud-kubernetes-configserver")
|
||||
.withNamespace("test").withLabels(new HashMap<>()).endMetadata().addNewSubset().addNewAddress()
|
||||
.withHostname("localhost").withIp("localhost").withNewTargetRef().withUid("10").endTargetRef()
|
||||
.endAddress().addNewPort("http", "http_tcp", wireMockServer.port(), "TCP").endSubset().build();
|
||||
|
||||
mockClient.endpoints().inNamespace("test").create(endPoint);
|
||||
|
||||
Service service = new ServiceBuilder().withNewMetadata().withName("spring-cloud-kubernetes-configserver")
|
||||
.withNamespace("test").withLabels(new HashMap<>()).endMetadata().build();
|
||||
|
||||
mockClient.services().inNamespace("test").create(service);
|
||||
|
||||
Environment environment = new Environment("test", "default");
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("hello", "world");
|
||||
org.springframework.cloud.config.environment.PropertySource p = new PropertySource("p1", properties);
|
||||
environment.add(p);
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
stubFor(get("/application/default")
|
||||
.willReturn(aResponse().withStatus(200).withBody(objectMapper.writeValueAsString(environment))
|
||||
.withHeader("content-type", "application/json")));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after() {
|
||||
wireMockServer.stop();
|
||||
mockClient.close();
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBootstrapper() {
|
||||
this.context = setup().run();
|
||||
verify(getRequestedFor(urlEqualTo("/application/default")));
|
||||
assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world");
|
||||
}
|
||||
|
||||
SpringApplicationBuilder setup(String... env) {
|
||||
SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class)
|
||||
.properties(addDefaultEnv(env));
|
||||
builder.addBootstrapRegistryInitializer(new Fabric8ConfigServerBootstrapper());
|
||||
return builder;
|
||||
}
|
||||
|
||||
private String[] addDefaultEnv(String[] env) {
|
||||
Set<String> 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:");
|
||||
set.add("spring.cloud.config.discovery.service-id=spring-cloud-kubernetes-configserver");
|
||||
return set.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user