Skip to content

Commit 7d42d8f

Browse files
Ryan Baxterryanjbaxter
andauthored
Support looking up the config server via discoveryclient (#1354)
Fixes #1021 Co-authored-by: Ryan Baxter <[email protected]>
1 parent 22fb83d commit 7d42d8f

File tree

17 files changed

+967
-26
lines changed

17 files changed

+967
-26
lines changed

spring-cloud-kubernetes-client-discovery/pom.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@
3838
<artifactId>spring-boot-starter-webflux</artifactId>
3939
<optional>true</optional>
4040
</dependency>
41+
<dependency>
42+
<groupId>org.springframework.cloud</groupId>
43+
<artifactId>spring-cloud-config-client</artifactId>
44+
<optional>true</optional>
45+
</dependency>
4146

4247
<!-- Testing Dependencies -->
4348
<dependency>
@@ -55,11 +60,6 @@
5560
<artifactId>spring-boot-starter-web</artifactId>
5661
<scope>test</scope>
5762
</dependency>
58-
<dependency>
59-
<groupId>org.springframework.cloud</groupId>
60-
<artifactId>spring-cloud-config-client</artifactId>
61-
<scope>test</scope>
62-
</dependency>
6363
<dependency>
6464
<groupId>io.projectreactor</groupId>
6565
<artifactId>reactor-test</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* Copyright 2019-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.kubernetes.client.discovery;
18+
19+
import java.util.Collections;
20+
import java.util.List;
21+
22+
import io.kubernetes.client.informer.SharedIndexInformer;
23+
import io.kubernetes.client.informer.SharedInformerFactory;
24+
import io.kubernetes.client.informer.cache.Lister;
25+
import io.kubernetes.client.openapi.ApiClient;
26+
import io.kubernetes.client.openapi.models.V1Endpoints;
27+
import io.kubernetes.client.openapi.models.V1EndpointsList;
28+
import io.kubernetes.client.openapi.models.V1Service;
29+
import io.kubernetes.client.openapi.models.V1ServiceList;
30+
import io.kubernetes.client.util.Namespaces;
31+
import io.kubernetes.client.util.generic.GenericKubernetesApi;
32+
import org.apache.commons.logging.Log;
33+
34+
import org.springframework.boot.BootstrapContext;
35+
import org.springframework.boot.BootstrapRegistry;
36+
import org.springframework.boot.context.properties.bind.BindHandler;
37+
import org.springframework.boot.context.properties.bind.Bindable;
38+
import org.springframework.boot.context.properties.bind.Binder;
39+
import org.springframework.cloud.client.ServiceInstance;
40+
import org.springframework.cloud.config.client.ConfigServerInstanceProvider;
41+
import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration;
42+
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
43+
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
44+
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper;
45+
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider;
46+
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
47+
import org.springframework.core.env.AbstractEnvironment;
48+
import org.springframework.core.env.Environment;
49+
import org.springframework.util.ClassUtils;
50+
51+
/**
52+
* @author Ryan Baxter
53+
*/
54+
class KubernetesClientConfigServerBootstrapper extends KubernetesConfigServerBootstrapper {
55+
56+
@Override
57+
public void initialize(BootstrapRegistry registry) {
58+
if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) {
59+
return;
60+
}
61+
// We need to pass a lambda here rather than create a new instance of
62+
// ConfigServerInstanceProvider.Function
63+
// or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on
64+
// the classpath
65+
registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create);
66+
}
67+
68+
final static class KubernetesFunction implements ConfigServerInstanceProvider.Function {
69+
70+
private final BootstrapContext context;
71+
72+
private KubernetesFunction(BootstrapContext context) {
73+
this.context = context;
74+
}
75+
76+
static KubernetesFunction create(BootstrapContext context) {
77+
return new KubernetesFunction(context);
78+
}
79+
80+
@Override
81+
public List<ServiceInstance> apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) {
82+
if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) {
83+
// If we don't have the Binder or BinderHandler from the
84+
// ConfigDataLocationResolverContext
85+
// we won't be able to create the necessary configuration
86+
// properties to configure the
87+
// Kubernetes DiscoveryClient
88+
return Collections.emptyList();
89+
}
90+
KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder,
91+
bindHandler);
92+
KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler);
93+
return getInstanceProvider(discoveryProperties, clientProperties, context, binder, bindHandler, log)
94+
.getInstances(serviceId);
95+
}
96+
97+
protected KubernetesConfigServerInstanceProvider getInstanceProvider(
98+
KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties,
99+
BootstrapContext context, Binder binder, BindHandler bindHandler, Log log) {
100+
if (context.isRegistered(KubernetesInformerDiscoveryClient.class)) {
101+
KubernetesInformerDiscoveryClient client = context.get(KubernetesInformerDiscoveryClient.class);
102+
return client::getInstances;
103+
}
104+
else {
105+
KubernetesClientAutoConfiguration clientAutoConfiguration = new KubernetesClientAutoConfiguration();
106+
ApiClient apiClient = context.getOrElseSupply(ApiClient.class,
107+
() -> clientAutoConfiguration.apiClient(clientProperties));
108+
109+
KubernetesNamespaceProvider kubernetesNamespaceProvider = clientAutoConfiguration
110+
.kubernetesNamespaceProvider(getNamespaceEnvironment(binder, bindHandler));
111+
112+
String namespace = getInformerNamespace(kubernetesNamespaceProvider, discoveryProperties);
113+
SharedInformerFactory sharedInformerFactory = new SharedInformerFactory(apiClient);
114+
SpringCloudKubernetesInformerFactoryProcessor informerFactoryProcessor = new SpringCloudKubernetesInformerFactoryProcessor(
115+
kubernetesNamespaceProvider, apiClient, sharedInformerFactory,
116+
discoveryProperties.isAllNamespaces());
117+
final GenericKubernetesApi<V1Service, V1ServiceList> servicesApi = new GenericKubernetesApi<>(
118+
V1Service.class, V1ServiceList.class, "", "v1", "services", apiClient);
119+
SharedIndexInformer<V1Service> serviceSharedIndexInformer = sharedInformerFactory
120+
.sharedIndexInformerFor(servicesApi, V1Service.class, 0L, namespace);
121+
Lister<V1Service> serviceLister = new Lister<>(serviceSharedIndexInformer.getIndexer());
122+
final GenericKubernetesApi<V1Endpoints, V1EndpointsList> endpointsApi = new GenericKubernetesApi<>(
123+
V1Endpoints.class, V1EndpointsList.class, "", "v1", "endpoints", apiClient);
124+
SharedIndexInformer<V1Endpoints> endpointsSharedIndexInformer = sharedInformerFactory
125+
.sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0L, namespace);
126+
Lister<V1Endpoints> endpointsLister = new Lister<>(endpointsSharedIndexInformer.getIndexer());
127+
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(
128+
kubernetesNamespaceProvider.getNamespace(), sharedInformerFactory, serviceLister,
129+
endpointsLister, serviceSharedIndexInformer, endpointsSharedIndexInformer, discoveryProperties);
130+
try {
131+
discoveryClient.afterPropertiesSet();
132+
return discoveryClient::getInstances;
133+
}
134+
catch (Exception e) {
135+
if (log != null) {
136+
log.warn("Error initiating informer discovery client", e);
137+
}
138+
return (serviceId) -> Collections.emptyList();
139+
}
140+
finally {
141+
sharedInformerFactory.stopAllRegisteredInformers();
142+
}
143+
}
144+
}
145+
146+
private String getInformerNamespace(KubernetesNamespaceProvider kubernetesNamespaceProvider,
147+
KubernetesDiscoveryProperties discoveryProperties) {
148+
return discoveryProperties.isAllNamespaces() ? Namespaces.NAMESPACE_ALL
149+
: kubernetesNamespaceProvider.getNamespace() == null ? Namespaces.NAMESPACE_DEFAULT
150+
: kubernetesNamespaceProvider.getNamespace();
151+
}
152+
153+
private Environment getNamespaceEnvironment(Binder binder, BindHandler bindHandler) {
154+
return new AbstractEnvironment() {
155+
@Override
156+
public String getProperty(String key) {
157+
return binder.bind(key, Bindable.of(String.class), bindHandler).orElse(super.getProperty(key));
158+
}
159+
};
160+
}
161+
162+
// This method should never be called, but is there for backward
163+
// compatibility purposes
164+
@Override
165+
public List<ServiceInstance> apply(String serviceId) {
166+
return apply(serviceId, null, null, null);
167+
}
168+
169+
}
170+
171+
}

spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInforme
55
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
66
org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientConfigClientBootstrapConfiguration
77

8+
org.springframework.boot.BootstrapRegistryInitializer=\
9+
org.springframework.cloud.kubernetes.client.discovery.KubernetesClientConfigServerBootstrapper
10+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright 2019-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.kubernetes.client.discovery;
18+
19+
import java.time.Duration;
20+
import java.util.Arrays;
21+
import java.util.HashMap;
22+
import java.util.LinkedHashSet;
23+
import java.util.Map;
24+
import java.util.Set;
25+
26+
import com.fasterxml.jackson.core.JsonProcessingException;
27+
import com.fasterxml.jackson.databind.ObjectMapper;
28+
import com.github.tomakehurst.wiremock.WireMockServer;
29+
import com.github.tomakehurst.wiremock.client.WireMock;
30+
import io.kubernetes.client.openapi.ApiClient;
31+
import io.kubernetes.client.openapi.JSON;
32+
import io.kubernetes.client.openapi.models.V1EndpointAddress;
33+
import io.kubernetes.client.openapi.models.V1EndpointPort;
34+
import io.kubernetes.client.openapi.models.V1EndpointSubset;
35+
import io.kubernetes.client.openapi.models.V1Endpoints;
36+
import io.kubernetes.client.openapi.models.V1EndpointsList;
37+
import io.kubernetes.client.openapi.models.V1EndpointsListBuilder;
38+
import io.kubernetes.client.openapi.models.V1ListMetaBuilder;
39+
import io.kubernetes.client.openapi.models.V1ObjectMeta;
40+
import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder;
41+
import io.kubernetes.client.openapi.models.V1ObjectReferenceBuilder;
42+
import io.kubernetes.client.openapi.models.V1ServiceBuilder;
43+
import io.kubernetes.client.openapi.models.V1ServiceList;
44+
import io.kubernetes.client.openapi.models.V1ServiceListBuilder;
45+
import io.kubernetes.client.openapi.models.V1ServicePortBuilder;
46+
import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder;
47+
import io.kubernetes.client.util.ClientBuilder;
48+
import org.junit.jupiter.api.AfterEach;
49+
import org.junit.jupiter.api.BeforeEach;
50+
import org.junit.jupiter.api.Test;
51+
52+
import org.springframework.boot.SpringBootConfiguration;
53+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
54+
import org.springframework.boot.builder.SpringApplicationBuilder;
55+
import org.springframework.cloud.config.environment.Environment;
56+
import org.springframework.cloud.config.environment.PropertySource;
57+
import org.springframework.context.ConfigurableApplicationContext;
58+
import org.springframework.context.annotation.Bean;
59+
60+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
61+
import static com.github.tomakehurst.wiremock.client.WireMock.get;
62+
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
63+
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
64+
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
65+
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
66+
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
67+
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
68+
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
69+
70+
/**
71+
* @author Ryan Baxter
72+
*/
73+
class KubernetesClientConfigServerBootstrapperTests {
74+
75+
private static WireMockServer wireMockServer;
76+
77+
private ConfigurableApplicationContext context;
78+
79+
@BeforeEach
80+
public void before() throws JsonProcessingException {
81+
wireMockServer = new WireMockServer(options().dynamicPort());
82+
wireMockServer.start();
83+
WireMock.configureFor(wireMockServer.port());
84+
85+
V1ServiceList SERVICE_LIST = new V1ServiceListBuilder()
86+
.withMetadata(new V1ListMetaBuilder().withResourceVersion("1").build())
87+
.addToItems(new V1ServiceBuilder()
88+
.withMetadata(new V1ObjectMetaBuilder().withName("spring-cloud-kubernetes-configserver")
89+
.withNamespace("default").withResourceVersion("0").addToLabels("beta", "true")
90+
.addToAnnotations("org.springframework.cloud", "true").withUid("0").build())
91+
.withSpec(new V1ServiceSpecBuilder().withClusterIP("localhost").withSessionAffinity("None")
92+
.withType("ClusterIP")
93+
.addToPorts(new V1ServicePortBuilder().withPort(wireMockServer.port()).withName("http")
94+
.withProtocol("TCP").withNewTargetPort(wireMockServer.port()).build())
95+
.build())
96+
.build())
97+
.build();
98+
99+
V1EndpointsList ENDPOINTS_LIST = new V1EndpointsListBuilder()
100+
.withMetadata(new V1ListMetaBuilder().withResourceVersion("0").build())
101+
.addToItems(new V1Endpoints()
102+
.metadata(new V1ObjectMeta().name("spring-cloud-kubernetes-configserver").namespace("default"))
103+
.addSubsetsItem(
104+
new V1EndpointSubset()
105+
.addPortsItem(new V1EndpointPort().port(wireMockServer.port()).name("http"))
106+
.addAddressesItem(new V1EndpointAddress().hostname("localhost").ip("localhost")
107+
.targetRef(new V1ObjectReferenceBuilder().withUid("uid1").build()))))
108+
.build();
109+
110+
Environment environment = new Environment("test", "default");
111+
Map<String, Object> properties = new HashMap<>();
112+
properties.put("hello", "world");
113+
org.springframework.cloud.config.environment.PropertySource p = new PropertySource("p1", properties);
114+
environment.add(p);
115+
ObjectMapper objectMapper = new ObjectMapper();
116+
stubFor(get("/application/default")
117+
.willReturn(aResponse().withStatus(200).withBody(objectMapper.writeValueAsString(environment))
118+
.withHeader("content-type", "application/json")));
119+
stubFor(get("/api/v1/namespaces/default/endpoints?resourceVersion=0&watch=false")
120+
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(ENDPOINTS_LIST))
121+
.withHeader("content-type", "application/json")));
122+
stubFor(get("/api/v1/namespaces/default/services?resourceVersion=0&watch=false")
123+
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SERVICE_LIST))
124+
.withHeader("content-type", "application/json")));
125+
stubFor(get(urlMatching("/api/v1/namespaces/default/services.*.watch=true"))
126+
.willReturn(aResponse().withStatus(200)));
127+
stubFor(get(urlMatching("/api/v1/namespaces/default/endpoints.*.watch=true"))
128+
.willReturn(aResponse().withStatus(200)));
129+
}
130+
131+
@AfterEach
132+
public void after() {
133+
wireMockServer.stop();
134+
context.close();
135+
}
136+
137+
@Test
138+
void testBootstrapper() {
139+
this.context = setup().run();
140+
verify(getRequestedFor(urlEqualTo("/application/default")));
141+
assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world");
142+
}
143+
144+
SpringApplicationBuilder setup(String... env) {
145+
SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class)
146+
.properties(addDefaultEnv(env));
147+
ApiClient apiClient = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port())
148+
.setReadTimeout(Duration.ZERO).build();
149+
builder.addBootstrapRegistryInitializer(registry -> registry.register(ApiClient.class, (context) -> apiClient));
150+
builder.addBootstrapRegistryInitializer(new KubernetesClientConfigServerBootstrapper());
151+
return builder;
152+
}
153+
154+
private String[] addDefaultEnv(String[] env) {
155+
Set<String> set = new LinkedHashSet<>();
156+
if (env != null && env.length > 0) {
157+
set.addAll(Arrays.asList(env));
158+
}
159+
set.add("spring.cloud.config.discovery.enabled=true");
160+
set.add("spring.config.import=optional:configserver:");
161+
set.add("spring.cloud.config.discovery.service-id=spring-cloud-kubernetes-configserver");
162+
set.add("spring.cloud.kubernetes.client.namespace=default");
163+
return set.toArray(new String[0]);
164+
}
165+
166+
@SpringBootConfiguration
167+
@EnableAutoConfiguration
168+
static class TestConfig {
169+
170+
@Bean
171+
public ApiClient apiClient() {
172+
return new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
173+
}
174+
175+
}
176+
177+
}

spring-cloud-kubernetes-commons/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@
6767
<artifactId>spring-boot-starter-aop</artifactId>
6868
<optional>true</optional>
6969
</dependency>
70+
<dependency>
71+
<groupId>org.springframework.cloud</groupId>
72+
<artifactId>spring-cloud-config-client</artifactId>
73+
<optional>true</optional>
74+
</dependency>
7075

7176
<dependency>
7277
<groupId>org.springframework.boot</groupId>

0 commit comments

Comments
 (0)