diff --git a/.github/workflows/composites/cache/action.yaml b/.github/workflows/composites/cache/action.yaml
index b823306e..a4504fb6 100644
--- a/.github/workflows/composites/cache/action.yaml
+++ b/.github/workflows/composites/cache/action.yaml
@@ -3,7 +3,7 @@ description: cache
runs:
using: "composite"
steps:
- - uses: actions/cache@v2
+ - uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-cache-${{ env.BRANCH_NAME }}-${{ hashFiles('**/pom.xml') }}
diff --git a/.github/workflows/composites/download-docker-images/action.yaml b/.github/workflows/composites/download-docker-images/action.yaml
index 038d2c65..0a26162e 100644
--- a/.github/workflows/composites/download-docker-images/action.yaml
+++ b/.github/workflows/composites/download-docker-images/action.yaml
@@ -6,7 +6,7 @@ description: download docker images
runs:
using: "composite"
steps:
- - uses: actions/cache@v2
+ - uses: actions/cache@v3
with:
path: /tmp/docker/images
key: docker-images-cache-${{ env.DOCKER_IMAGES_KEY }}
diff --git a/.github/workflows/composites/setup-jdk1.8/action.yaml b/.github/workflows/composites/setup-jdk1.8/action.yaml
index 900a3a19..7dab6375 100755
--- a/.github/workflows/composites/setup-jdk1.8/action.yaml
+++ b/.github/workflows/composites/setup-jdk1.8/action.yaml
@@ -3,7 +3,7 @@ description: setup project with jdk-8
runs:
using: "composite"
steps:
- - uses: actions/setup-java@v2
+ - uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '8'
diff --git a/.github/workflows/composites/setup-jdk17/action.yaml b/.github/workflows/composites/setup-jdk17/action.yaml
index 776a68df..19dcd374 100755
--- a/.github/workflows/composites/setup-jdk17/action.yaml
+++ b/.github/workflows/composites/setup-jdk17/action.yaml
@@ -3,7 +3,7 @@ description: setup project with jdk-17
runs:
using: "composite"
steps:
- - uses: actions/setup-java@v2
+ - uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
diff --git a/.github/workflows/composites/upload-docker-images/action.yaml b/.github/workflows/composites/upload-docker-images/action.yaml
index b0506b82..ed7bf1ea 100644
--- a/.github/workflows/composites/upload-docker-images/action.yaml
+++ b/.github/workflows/composites/upload-docker-images/action.yaml
@@ -6,7 +6,7 @@ description: upload docker images
runs:
using: "composite"
steps:
- - uses: actions/cache@v2
+ - uses: actions/cache@v3
with:
path: /tmp/docker/images
key: docker-images-cache-${{ env.DOCKER_IMAGES_KEY }}
diff --git a/spring-cloud-kubernetes-client-discovery/pom.xml b/spring-cloud-kubernetes-client-discovery/pom.xml
index 6501c41e..e982e0a6 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..282b8438
--- /dev/null
+++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapper.java
@@ -0,0 +1,173 @@
+/*
+ * 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;
+
+import static org.springframework.cloud.kubernetes.client.KubernetesClientUtils.kubernetesApiClient;
+
+/**
+ * @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 {
+
+ ApiClient defaultApiClient = kubernetesApiClient();
+ defaultApiClient.setUserAgent(binder.bind("spring.cloud.kubernetes.client.user-agent", String.class)
+ .orElse(KubernetesClientProperties.DEFAULT_USER_AGENT));
+ KubernetesClientAutoConfiguration clientAutoConfiguration = new KubernetesClientAutoConfiguration();
+ ApiClient apiClient = context.getOrElseSupply(ApiClient.class, () -> defaultApiClient);
+
+ KubernetesNamespaceProvider kubernetesNamespaceProvider = clientAutoConfiguration
+ .kubernetesNamespaceProvider(getNamespaceEnvironment(binder, bindHandler));
+
+ String namespace = getInformerNamespace(kubernetesNamespaceProvider, discoveryProperties);
+ SharedInformerFactory sharedInformerFactory = new SharedInformerFactory(apiClient);
+ 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(
+ 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.allNamespaces() ? 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 31842ad6..f1630371 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
@@ -1,3 +1,6 @@
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..76f895da
--- /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.CoreV1EndpointPort;
+import io.kubernetes.client.openapi.models.V1EndpointAddress;
+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 CoreV1EndpointPort().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 ef45de4e..08485ab8 100644
--- a/spring-cloud-kubernetes-commons/pom.xml
+++ b/spring-cloud-kubernetes-commons/pom.xml
@@ -75,6 +75,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/KubernetesNamespaceProvider.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/KubernetesNamespaceProvider.java
index ed5feca6..fa8d2ab4 100644
--- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/KubernetesNamespaceProvider.java
+++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/KubernetesNamespaceProvider.java
@@ -21,8 +21,12 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+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.boot.logging.DeferredLog;
import org.springframework.core.env.Environment;
+import org.springframework.util.ObjectUtils;
import static org.springframework.cloud.kubernetes.commons.KubernetesClientProperties.SERVICE_ACCOUNT_NAMESPACE_PATH;
@@ -43,23 +47,49 @@ public class KubernetesNamespaceProvider {
*/
public static final String NAMESPACE_PATH_PROPERTY = "spring.cloud.kubernetes.client.serviceAccountNamespacePath";
+ private BindHandler bindHandler;
+
private String serviceAccountNamespace;
- private final Environment environment;
+ private Environment environment;
+
+ private Binder binder;
public KubernetesNamespaceProvider(Environment env) {
this.environment = env;
LOG.replayTo(KubernetesNamespaceProvider.class);
}
+ public KubernetesNamespaceProvider(Binder binder, BindHandler bindHandler) {
+ this.binder = binder;
+ this.bindHandler = bindHandler;
+ }
+
public String getNamespace() {
- String namespace = environment.getProperty(NAMESPACE_PROPERTY);
+ String namespace = null;
+ if (environment != null) {
+ namespace = environment.getProperty(NAMESPACE_PROPERTY);
+ }
+ if (ObjectUtils.isEmpty(namespace) && binder != null) {
+ namespace = binder.bind(NAMESPACE_PROPERTY, String.class).orElse(null);
+ }
return namespace != null ? namespace : getServiceAccountNamespace();
}
private String getServiceAccountNamespace() {
- String serviceAccountNamespacePathString = environment.getProperty(NAMESPACE_PATH_PROPERTY,
- SERVICE_ACCOUNT_NAMESPACE_PATH);
+ String serviceAccountNamespacePathString = null;
+ if (environment != null) {
+ serviceAccountNamespacePathString = environment.getProperty(NAMESPACE_PATH_PROPERTY,
+ SERVICE_ACCOUNT_NAMESPACE_PATH);
+ }
+ if (ObjectUtils.isEmpty(serviceAccountNamespacePathString) && binder != null) {
+ // When using the binder we cannot use camelcase properties, it considers them
+ // invalid
+ serviceAccountNamespacePathString = binder
+ .bind("spring.cloud.kubernetes.client.service-account-namespace-path", Bindable.of(String.class),
+ bindHandler)
+ .orElse(SERVICE_ACCOUNT_NAMESPACE_PATH);
+ }
if (serviceAccountNamespace == null) {
serviceAccountNamespace = getNamespaceFromServiceAccountFile(serviceAccountNamespacePathString);
}
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..3e829899
--- /dev/null
+++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerBootstrapper.java
@@ -0,0 +1,54 @@
+/*
+ * 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.KubernetesNamespaceProvider;
+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.DEFAULT);
+ }
+
+ public static KubernetesClientProperties createKubernetesClientProperties(Binder binder, BindHandler bindHandler) {
+ return binder.bindOrCreate(KubernetesClientProperties.PREFIX, Bindable.of(KubernetesClientProperties.class))
+ .withNamespace(new KubernetesNamespaceProvider(binder, bindHandler).getNamespace());
+ }
+
+ 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 4c6d9b62..13477539 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
@@ -28,7 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
classes = KubernetesCommonsAutoConfigurationTests.App.class,
properties = { "spring.main.cloud-platform=KUBERNETES", "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" })
class KubernetesCommonsAutoConfigurationTests {
@Autowired
diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigAndSecretsFailFastEnabledWithDefaultRetryConfiguration.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigAndSecretsFailFastEnabledWithDefaultRetryConfiguration.java
index 9e12e22d..a9d8755d 100644
--- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigAndSecretsFailFastEnabledWithDefaultRetryConfiguration.java
+++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigAndSecretsFailFastEnabledWithDefaultRetryConfiguration.java
@@ -36,7 +36,8 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class,
properties = { "spring.cloud.kubernetes.config.fail-fast=true",
- "spring.cloud.kubernetes.secrets.fail-fast=true", "spring.main.cloud-platform=KUBERNETES" })
+ "spring.cloud.kubernetes.secrets.fail-fast=true", "spring.main.cloud-platform=KUBERNETES",
+ "spring.cloud.config.enabled=false" })
class ConfigAndSecretsFailFastEnabledWithDefaultRetryConfiguration {
@Autowired
diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabled.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabled.java
index 8b564c56..9efbfcd8 100644
--- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabled.java
+++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabled.java
@@ -34,7 +34,8 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Isik Erhan
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class,
- properties = { "spring.cloud.kubernetes.config.fail-fast=true", "spring.main.cloud-platform=KUBERNETES" })
+ properties = { "spring.cloud.kubernetes.config.fail-fast=true", "spring.main.cloud-platform=KUBERNETES",
+ "spring.cloud.config.enabled=false" })
class ConfigFailFastEnabled {
@Autowired
diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledButRetryDisabled.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledButRetryDisabled.java
index 84234d29..4a798e99 100644
--- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledButRetryDisabled.java
+++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledButRetryDisabled.java
@@ -29,8 +29,9 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Isik Erhan
*/
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class, properties = {
- "spring.cloud.kubernetes.config.fail-fast=true", "spring.cloud.kubernetes.config.retry.enabled=false" })
+@SpringBootTest(webEnvironment = SpringBootTest.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" })
class ConfigFailFastEnabledButRetryDisabled {
@Autowired
diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledWithCustomRetryConfiguration.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledWithCustomRetryConfiguration.java
index 52e3c355..9dadffa6 100644
--- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledWithCustomRetryConfiguration.java
+++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledWithCustomRetryConfiguration.java
@@ -29,12 +29,11 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Isik Erhan
*/
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class,
- properties = { "spring.cloud.kubernetes.config.fail-fast=true",
- "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.main.cloud-platform=KUBERNETES" })
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class, properties = {
+ "spring.cloud.kubernetes.config.fail-fast=true", "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.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false" })
class ConfigFailFastEnabledWithCustomRetryConfiguration {
@Autowired
diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/FailFastDisabled.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/FailFastDisabled.java
index 63ab8a0c..9cbd6980 100644
--- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/FailFastDisabled.java
+++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/FailFastDisabled.java
@@ -29,7 +29,8 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Isik Erhan
*/
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class,
+ properties = { "spring.cloud.config.enabled=false" })
class FailFastDisabled {
@Autowired
diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabled.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabled.java
index a640da72..8263961c 100644
--- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabled.java
+++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabled.java
@@ -34,7 +34,8 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Isik Erhan
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class,
- properties = { "spring.cloud.kubernetes.secrets.fail-fast=true", "spring.main.cloud-platform=KUBERNETES" })
+ properties = { "spring.cloud.kubernetes.secrets.fail-fast=true", "spring.main.cloud-platform=KUBERNETES",
+ "spring.cloud.config.enabled=false" })
class SecretsFailFastEnabled {
@Autowired
diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledButRetryDisabled.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledButRetryDisabled.java
index 6925dd8f..8b32ddc1 100644
--- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledButRetryDisabled.java
+++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledButRetryDisabled.java
@@ -29,8 +29,9 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Isik Erhan
*/
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class, properties = {
- "spring.cloud.kubernetes.secrets.fail-fast=true", "spring.cloud.kubernetes.secrets.retry.enabled=false" })
+@SpringBootTest(webEnvironment = SpringBootTest.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" })
class SecretsFailFastEnabledButRetryDisabled {
@Autowired
diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledWithCustomRetryConfiguration.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledWithCustomRetryConfiguration.java
index b4d54048..51e0b204 100644
--- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledWithCustomRetryConfiguration.java
+++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledWithCustomRetryConfiguration.java
@@ -34,7 +34,8 @@ import static org.assertj.core.api.Assertions.assertThat;
"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.main.cloud-platform=KUBERNETES" })
+ "spring.cloud.kubernetes.secrets.retry.multiplier=1.5", "spring.main.cloud-platform=KUBERNETES",
+ "spring.cloud.config.enabled=false" })
class SecretsFailFastEnabledWithCustomRetryConfiguration {
@Autowired
diff --git a/spring-cloud-kubernetes-discovery/pom.xml b/spring-cloud-kubernetes-discovery/pom.xml
index 668a02d8..2325187a 100644
--- a/spring-cloud-kubernetes-discovery/pom.xml
+++ b/spring-cloud-kubernetes-discovery/pom.xml
@@ -21,6 +21,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
new file mode 100644
index 00000000..4fbb2223
--- /dev/null
+++ b/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+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 79ae9f35..b509b182 100644
--- a/spring-cloud-kubernetes-fabric8-discovery/pom.xml
+++ b/spring-cloud-kubernetes-fabric8-discovery/pom.xml
@@ -42,6 +42,11 @@
org.springframework.cloud
spring-cloud-context
+
+ org.springframework.cloud
+ spring-cloud-config-client
+ true
+
@@ -102,13 +107,13 @@
test
- org.springframework.cloud
- spring-cloud-config-client
+ io.projectreactor
+ reactor-test
test
- io.projectreactor
- reactor-test
+ com.github.tomakehurst
+ wiremock-jre8-standalone
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..17231f86
--- /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, binder, bindHandler).getInstances(serviceId);
+ }
+
+ private KubernetesConfigServerInstanceProvider getInstanceProvider(
+ KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties,
+ BootstrapContext context, Binder binder, BindHandler bindHandler) {
+ 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);
+ KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(kubernetesClient,
+ discoveryProperties,
+ KubernetesClientServicesFunctionProvider.servicesFunction(discoveryProperties, binder, bindHandler), null,
+ 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/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java
index 583db5e9..820d3a3f 100644
--- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java
+++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java
@@ -16,6 +16,8 @@
package org.springframework.cloud.kubernetes.fabric8.discovery;
+import org.springframework.boot.context.properties.bind.BindHandler;
+import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.fabric8.Fabric8Utils;
@@ -44,4 +46,19 @@ public final class KubernetesClientServicesFunctionProvider {
}
+ public static KubernetesClientServicesFunction servicesFunction(KubernetesDiscoveryProperties properties,
+ Binder binder, BindHandler bindHandler) {
+
+ if (properties.allNamespaces()) {
+ return (client) -> client.services().inAnyNamespace().withLabels(properties.serviceLabels());
+ }
+
+ return client -> {
+ String namespace = Fabric8Utils.getApplicationNamespace(client, null, "discovery-service",
+ new KubernetesNamespaceProvider(binder, bindHandler));
+ return client.services().inNamespace(namespace).withLabels(properties.serviceLabels());
+ };
+
+ }
+
}
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 7262a091..d1cfd557 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
@@ -1,2 +1,4 @@
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..b1420b2f
--- /dev/null
+++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapperTests.java
@@ -0,0 +1,145 @@
+/*
+ * 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.api.model.ServiceSpecBuilder;
+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()
+ .withSpec(new ServiceSpecBuilder().withType("NodePort").build()).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");
+ set.add("spring.cloud.kubernetes.client.namespace=test");
+ return set.toArray(new String[0]);
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ static class TestConfig {
+
+ }
+
+}