diff --git a/spring-cloud-kubernetes-client-discovery/pom.xml b/spring-cloud-kubernetes-client-discovery/pom.xml
index 0a1a1f7e..4ccc540f 100644
--- a/spring-cloud-kubernetes-client-discovery/pom.xml
+++ b/spring-cloud-kubernetes-client-discovery/pom.xml
@@ -38,6 +38,11 @@
spring-boot-starter-webflux
true
+
+ org.springframework.cloud
+ spring-cloud-config-client
+ true
+
@@ -55,11 +60,6 @@
spring-boot-starter-web
test
-
- org.springframework.cloud
- spring-cloud-config-client
- test
-
io.projectreactor
reactor-test
diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapper.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapper.java
new file mode 100644
index 00000000..cca70701
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapper.java
@@ -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 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 servicesApi = new GenericKubernetesApi<>(
+ V1Service.class, V1ServiceList.class, "", "v1", "services", apiClient);
+ SharedIndexInformer serviceSharedIndexInformer = sharedInformerFactory
+ .sharedIndexInformerFor(servicesApi, V1Service.class, 0L, namespace);
+ Lister serviceLister = new Lister<>(serviceSharedIndexInformer.getIndexer());
+ final GenericKubernetesApi endpointsApi = new GenericKubernetesApi<>(
+ V1Endpoints.class, V1EndpointsList.class, "", "v1", "endpoints", apiClient);
+ SharedIndexInformer endpointsSharedIndexInformer = sharedInformerFactory
+ .sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0L, namespace);
+ Lister 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 apply(String serviceId) {
+ return apply(serviceId, null, null, null);
+ }
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories
index 507576df..8e4f0054 100644
--- a/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories
+++ b/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories
@@ -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
+
diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapperTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapperTests.java
new file mode 100644
index 00000000..56057e01
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapperTests.java
@@ -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 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 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();
+ }
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-commons/pom.xml b/spring-cloud-kubernetes-commons/pom.xml
index 6d020f04..70b4116d 100644
--- a/spring-cloud-kubernetes-commons/pom.xml
+++ b/spring-cloud-kubernetes-commons/pom.xml
@@ -67,6 +67,11 @@
spring-boot-starter-aop
true
+
+ org.springframework.cloud
+ spring-cloud-config-client
+ true
+
org.springframework.boot
diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerBootstrapper.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerBootstrapper.java
new file mode 100644
index 00000000..de6d9049
--- /dev/null
+++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerBootstrapper.java
@@ -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);
+ }
+
+}
diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerInstanceProvider.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerInstanceProvider.java
new file mode 100644
index 00000000..c71e78a8
--- /dev/null
+++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerInstanceProvider.java
@@ -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 getInstances(String serviceId);
+
+}
diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsAutoConfigurationTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsAutoConfigurationTests.java
index fca088cf..c0b90a22 100644
--- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsAutoConfigurationTests.java
+++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsAutoConfigurationTests.java
@@ -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
diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesBootstrapConfigurationTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesBootstrapConfigurationTests.java
index 1d41e7f0..7356c295 100644
--- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesBootstrapConfigurationTests.java
+++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesBootstrapConfigurationTests.java
@@ -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 {
-
- }
-
}
diff --git a/spring-cloud-kubernetes-discovery/pom.xml b/spring-cloud-kubernetes-discovery/pom.xml
index 19ae635d..8262de42 100644
--- a/spring-cloud-kubernetes-discovery/pom.xml
+++ b/spring-cloud-kubernetes-discovery/pom.xml
@@ -24,6 +24,15 @@
org.springframework.cloud
spring-cloud-commons
+
+ org.springframework.cloud
+ spring-cloud-kubernetes-commons
+
+
+ org.springframework.cloud
+ spring-cloud-config-client
+ true
+
org.springframework.boot
spring-boot-actuator
diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapper.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapper.java
new file mode 100644
index 00000000..a68d89b8
--- /dev/null
+++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapper.java
@@ -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 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 apply(String serviceId) {
+ return apply(serviceId, null, null, null);
+ }
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories
index a82e049f..7cb0c2ab 100644
--- a/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories
+++ b/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories
@@ -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
diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapperTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapperTests.java
new file mode 100644
index 00000000..bf1d47e0
--- /dev/null
+++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapperTests.java
@@ -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 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 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 {
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-fabric8-discovery/pom.xml b/spring-cloud-kubernetes-fabric8-discovery/pom.xml
index 1ac11a80..4ee3d228 100644
--- a/spring-cloud-kubernetes-fabric8-discovery/pom.xml
+++ b/spring-cloud-kubernetes-fabric8-discovery/pom.xml
@@ -60,6 +60,11 @@
org.springframework.cloud
spring-cloud-context
+
+ org.springframework.cloud
+ spring-cloud-config-client
+ true
+
@@ -117,13 +122,13 @@
test
- org.springframework.cloud
- spring-cloud-config-client
+ io.projectreactor
+ reactor-test
test
- io.projectreactor
- reactor-test
+ com.github.tomakehurst
+ wiremock-jre8
test
diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapper.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapper.java
new file mode 100644
index 00000000..067762e5
--- /dev/null
+++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapper.java
@@ -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 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 apply(String serviceId) {
+ return apply(serviceId, null, null, null);
+ }
+
+ }
+
+}
diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-fabric8-discovery/src/main/resources/META-INF/spring.factories
index 4746c884..ebe17137 100644
--- a/spring-cloud-kubernetes-fabric8-discovery/src/main/resources/META-INF/spring.factories
+++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/resources/META-INF/spring.factories
@@ -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
diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapperTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapperTests.java
new file mode 100644
index 00000000..e9e66de3
--- /dev/null
+++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapperTests.java
@@ -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 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 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 {
+
+ }
+
+}