Support looking up the config server via discoveryclient (#1354)

Fixes #1021

Co-authored-by: Ryan Baxter <524254+ryanjbaxter@users.noreply.github.com>
This commit is contained in:
Ryan Baxter
2023-06-13 19:09:05 -04:00
committed by GitHub
parent 22fb83d3c1
commit 7d42d8ff1c
17 changed files with 967 additions and 26 deletions

View File

@@ -38,6 +38,11 @@
<artifactId>spring-boot-starter-webflux</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
<optional>true</optional>
</dependency>
<!-- Testing Dependencies -->
<dependency>
@@ -55,11 +60,6 @@
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2019-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.client.discovery;
import java.util.Collections;
import java.util.List;
import io.kubernetes.client.informer.SharedIndexInformer;
import io.kubernetes.client.informer.SharedInformerFactory;
import io.kubernetes.client.informer.cache.Lister;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.models.V1Endpoints;
import io.kubernetes.client.openapi.models.V1EndpointsList;
import io.kubernetes.client.openapi.models.V1Service;
import io.kubernetes.client.openapi.models.V1ServiceList;
import io.kubernetes.client.util.Namespaces;
import io.kubernetes.client.util.generic.GenericKubernetesApi;
import org.apache.commons.logging.Log;
import org.springframework.boot.BootstrapContext;
import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.config.client.ConfigServerInstanceProvider;
import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration;
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider;
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper;
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.util.ClassUtils;
/**
* @author Ryan Baxter
*/
class KubernetesClientConfigServerBootstrapper extends KubernetesConfigServerBootstrapper {
@Override
public void initialize(BootstrapRegistry registry) {
if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) {
return;
}
// We need to pass a lambda here rather than create a new instance of
// ConfigServerInstanceProvider.Function
// or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on
// the classpath
registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create);
}
final static class KubernetesFunction implements ConfigServerInstanceProvider.Function {
private final BootstrapContext context;
private KubernetesFunction(BootstrapContext context) {
this.context = context;
}
static KubernetesFunction create(BootstrapContext context) {
return new KubernetesFunction(context);
}
@Override
public List<ServiceInstance> apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) {
if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) {
// If we don't have the Binder or BinderHandler from the
// ConfigDataLocationResolverContext
// we won't be able to create the necessary configuration
// properties to configure the
// Kubernetes DiscoveryClient
return Collections.emptyList();
}
KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder,
bindHandler);
KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler);
return getInstanceProvider(discoveryProperties, clientProperties, context, binder, bindHandler, log)
.getInstances(serviceId);
}
protected KubernetesConfigServerInstanceProvider getInstanceProvider(
KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties,
BootstrapContext context, Binder binder, BindHandler bindHandler, Log log) {
if (context.isRegistered(KubernetesInformerDiscoveryClient.class)) {
KubernetesInformerDiscoveryClient client = context.get(KubernetesInformerDiscoveryClient.class);
return client::getInstances;
}
else {
KubernetesClientAutoConfiguration clientAutoConfiguration = new KubernetesClientAutoConfiguration();
ApiClient apiClient = context.getOrElseSupply(ApiClient.class,
() -> clientAutoConfiguration.apiClient(clientProperties));
KubernetesNamespaceProvider kubernetesNamespaceProvider = clientAutoConfiguration
.kubernetesNamespaceProvider(getNamespaceEnvironment(binder, bindHandler));
String namespace = getInformerNamespace(kubernetesNamespaceProvider, discoveryProperties);
SharedInformerFactory sharedInformerFactory = new SharedInformerFactory(apiClient);
SpringCloudKubernetesInformerFactoryProcessor informerFactoryProcessor = new SpringCloudKubernetesInformerFactoryProcessor(
kubernetesNamespaceProvider, apiClient, sharedInformerFactory,
discoveryProperties.isAllNamespaces());
final GenericKubernetesApi<V1Service, V1ServiceList> servicesApi = new GenericKubernetesApi<>(
V1Service.class, V1ServiceList.class, "", "v1", "services", apiClient);
SharedIndexInformer<V1Service> serviceSharedIndexInformer = sharedInformerFactory
.sharedIndexInformerFor(servicesApi, V1Service.class, 0L, namespace);
Lister<V1Service> serviceLister = new Lister<>(serviceSharedIndexInformer.getIndexer());
final GenericKubernetesApi<V1Endpoints, V1EndpointsList> endpointsApi = new GenericKubernetesApi<>(
V1Endpoints.class, V1EndpointsList.class, "", "v1", "endpoints", apiClient);
SharedIndexInformer<V1Endpoints> endpointsSharedIndexInformer = sharedInformerFactory
.sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0L, namespace);
Lister<V1Endpoints> endpointsLister = new Lister<>(endpointsSharedIndexInformer.getIndexer());
KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient(
kubernetesNamespaceProvider.getNamespace(), sharedInformerFactory, serviceLister,
endpointsLister, serviceSharedIndexInformer, endpointsSharedIndexInformer, discoveryProperties);
try {
discoveryClient.afterPropertiesSet();
return discoveryClient::getInstances;
}
catch (Exception e) {
if (log != null) {
log.warn("Error initiating informer discovery client", e);
}
return (serviceId) -> Collections.emptyList();
}
finally {
sharedInformerFactory.stopAllRegisteredInformers();
}
}
}
private String getInformerNamespace(KubernetesNamespaceProvider kubernetesNamespaceProvider,
KubernetesDiscoveryProperties discoveryProperties) {
return discoveryProperties.isAllNamespaces() ? Namespaces.NAMESPACE_ALL
: kubernetesNamespaceProvider.getNamespace() == null ? Namespaces.NAMESPACE_DEFAULT
: kubernetesNamespaceProvider.getNamespace();
}
private Environment getNamespaceEnvironment(Binder binder, BindHandler bindHandler) {
return new AbstractEnvironment() {
@Override
public String getProperty(String key) {
return binder.bind(key, Bindable.of(String.class), bindHandler).orElse(super.getProperty(key));
}
};
}
// This method should never be called, but is there for backward
// compatibility purposes
@Override
public List<ServiceInstance> apply(String serviceId) {
return apply(serviceId, null, null, null);
}
}
}

View File

@@ -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

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2019-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.client.discovery;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.openapi.models.V1EndpointAddress;
import io.kubernetes.client.openapi.models.V1EndpointPort;
import io.kubernetes.client.openapi.models.V1EndpointSubset;
import io.kubernetes.client.openapi.models.V1Endpoints;
import io.kubernetes.client.openapi.models.V1EndpointsList;
import io.kubernetes.client.openapi.models.V1EndpointsListBuilder;
import io.kubernetes.client.openapi.models.V1ListMetaBuilder;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder;
import io.kubernetes.client.openapi.models.V1ObjectReferenceBuilder;
import io.kubernetes.client.openapi.models.V1ServiceBuilder;
import io.kubernetes.client.openapi.models.V1ServiceList;
import io.kubernetes.client.openapi.models.V1ServiceListBuilder;
import io.kubernetes.client.openapi.models.V1ServicePortBuilder;
import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder;
import io.kubernetes.client.util.ClientBuilder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
/**
* @author Ryan Baxter
*/
class KubernetesClientConfigServerBootstrapperTests {
private static WireMockServer wireMockServer;
private ConfigurableApplicationContext context;
@BeforeEach
public void before() throws JsonProcessingException {
wireMockServer = new WireMockServer(options().dynamicPort());
wireMockServer.start();
WireMock.configureFor(wireMockServer.port());
V1ServiceList SERVICE_LIST = new V1ServiceListBuilder()
.withMetadata(new V1ListMetaBuilder().withResourceVersion("1").build())
.addToItems(new V1ServiceBuilder()
.withMetadata(new V1ObjectMetaBuilder().withName("spring-cloud-kubernetes-configserver")
.withNamespace("default").withResourceVersion("0").addToLabels("beta", "true")
.addToAnnotations("org.springframework.cloud", "true").withUid("0").build())
.withSpec(new V1ServiceSpecBuilder().withClusterIP("localhost").withSessionAffinity("None")
.withType("ClusterIP")
.addToPorts(new V1ServicePortBuilder().withPort(wireMockServer.port()).withName("http")
.withProtocol("TCP").withNewTargetPort(wireMockServer.port()).build())
.build())
.build())
.build();
V1EndpointsList ENDPOINTS_LIST = new V1EndpointsListBuilder()
.withMetadata(new V1ListMetaBuilder().withResourceVersion("0").build())
.addToItems(new V1Endpoints()
.metadata(new V1ObjectMeta().name("spring-cloud-kubernetes-configserver").namespace("default"))
.addSubsetsItem(
new V1EndpointSubset()
.addPortsItem(new V1EndpointPort().port(wireMockServer.port()).name("http"))
.addAddressesItem(new V1EndpointAddress().hostname("localhost").ip("localhost")
.targetRef(new V1ObjectReferenceBuilder().withUid("uid1").build()))))
.build();
Environment environment = new Environment("test", "default");
Map<String, Object> properties = new HashMap<>();
properties.put("hello", "world");
org.springframework.cloud.config.environment.PropertySource p = new PropertySource("p1", properties);
environment.add(p);
ObjectMapper objectMapper = new ObjectMapper();
stubFor(get("/application/default")
.willReturn(aResponse().withStatus(200).withBody(objectMapper.writeValueAsString(environment))
.withHeader("content-type", "application/json")));
stubFor(get("/api/v1/namespaces/default/endpoints?resourceVersion=0&watch=false")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(ENDPOINTS_LIST))
.withHeader("content-type", "application/json")));
stubFor(get("/api/v1/namespaces/default/services?resourceVersion=0&watch=false")
.willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SERVICE_LIST))
.withHeader("content-type", "application/json")));
stubFor(get(urlMatching("/api/v1/namespaces/default/services.*.watch=true"))
.willReturn(aResponse().withStatus(200)));
stubFor(get(urlMatching("/api/v1/namespaces/default/endpoints.*.watch=true"))
.willReturn(aResponse().withStatus(200)));
}
@AfterEach
public void after() {
wireMockServer.stop();
context.close();
}
@Test
void testBootstrapper() {
this.context = setup().run();
verify(getRequestedFor(urlEqualTo("/application/default")));
assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world");
}
SpringApplicationBuilder setup(String... env) {
SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class)
.properties(addDefaultEnv(env));
ApiClient apiClient = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port())
.setReadTimeout(Duration.ZERO).build();
builder.addBootstrapRegistryInitializer(registry -> registry.register(ApiClient.class, (context) -> apiClient));
builder.addBootstrapRegistryInitializer(new KubernetesClientConfigServerBootstrapper());
return builder;
}
private String[] addDefaultEnv(String[] env) {
Set<String> set = new LinkedHashSet<>();
if (env != null && env.length > 0) {
set.addAll(Arrays.asList(env));
}
set.add("spring.cloud.config.discovery.enabled=true");
set.add("spring.config.import=optional:configserver:");
set.add("spring.cloud.config.discovery.service-id=spring-cloud-kubernetes-configserver");
set.add("spring.cloud.kubernetes.client.namespace=default");
return set.toArray(new String[0]);
}
@SpringBootConfiguration
@EnableAutoConfiguration
static class TestConfig {
@Bean
public ApiClient apiClient() {
return new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build();
}
}
}

View File

@@ -67,6 +67,11 @@
<artifactId>spring-boot-starter-aop</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -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);
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.commons.config;
import java.util.List;
import org.springframework.cloud.client.ServiceInstance;
/**
* @author Ryan Baxter
*/
public interface KubernetesConfigServerInstanceProvider {
List<ServiceInstance> getInstances(String serviceId);
}

View File

@@ -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

View File

@@ -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 {
}
}

View File

@@ -24,6 +24,15 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2013-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.discovery;
import java.util.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
import org.springframework.boot.BootstrapContext;
import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.config.client.ConfigServerInstanceProvider;
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper;
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.util.ClassUtils;
/**
* @author Ryan Baxter
*/
class ConfigServerBootstrapper extends KubernetesConfigServerBootstrapper {
@Override
public void initialize(BootstrapRegistry registry) {
if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) {
return;
}
// We need to pass a lambda here rather than create a new instance of
// ConfigServerInstanceProvider.Function
// or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on
// the classpath
registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create);
}
final static class KubernetesFunction implements ConfigServerInstanceProvider.Function {
private final BootstrapContext context;
private KubernetesFunction(BootstrapContext context) {
this.context = context;
}
static KubernetesFunction create(BootstrapContext context) {
return new KubernetesFunction(context);
}
@Override
public List<ServiceInstance> apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) {
if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) {
// If we don't have the Binder or BinderHandler from the
// ConfigDataLocationResolverContext
// we won't be able to create the necessary configuration
// properties to configure the
// Kubernetes DiscoveryClient
return Collections.emptyList();
}
KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder,
bindHandler);
KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler);
return getInstanceProvider(discoveryProperties, clientProperties, context, binder, bindHandler, log)
.getInstances(serviceId);
}
private KubernetesConfigServerInstanceProvider getInstanceProvider(
KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties,
BootstrapContext context, Binder binder, BindHandler bindHandler, Log log) {
KubernetesDiscoveryClientProperties kubernetesDiscoveryClientProperties = binder
.bind("spring.cloud.kubernetes.discovery", Bindable.of(KubernetesDiscoveryClientProperties.class),
bindHandler)
.orElseGet(KubernetesDiscoveryClientProperties::new);
KubernetesDiscoveryClientAutoConfiguration.Servlet autoConfiguration = new KubernetesDiscoveryClientAutoConfiguration.Servlet();
DiscoveryClient discoveryClient = autoConfiguration
.kubernetesDiscoveryClient(autoConfiguration.restTemplate(), kubernetesDiscoveryClientProperties);
return discoveryClient::getInstances;
}
// This method should never be called, but is there for backward
// compatibility purposes
@Override
public List<ServiceInstance> apply(String serviceId) {
return apply(serviceId, null, null, null);
}
}
}

View File

@@ -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

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2013-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.discovery;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.context.ConfigurableApplicationContext;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Ryan Baxter
*/
class ConfigServerBootstrapperTests {
private static WireMockServer wireMockServer;
protected ConfigurableApplicationContext context;
@AfterEach
void close() {
wireMockServer.stop();
if (this.context != null) {
this.context.close();
}
}
@BeforeEach
void beforeAll() throws JsonProcessingException {
wireMockServer = new WireMockServer(options().dynamicPort());
wireMockServer.start();
WireMock.configureFor(wireMockServer.port());
String APPS_NAME = "[{\"instanceId\":\"uid2\",\"serviceId\":\"spring-cloud-kubernetes-configserver\",\"host\":\"localhost\",\"port\":"
+ wireMockServer.port() + ",\"uri\":\"" + wireMockServer.baseUrl()
+ "\",\"secure\":false,\"metadata\":{\"spring\":\"true\",\"http\":\"8080\",\"k8s\":\"true\"},\"namespace\":\"namespace1\",\"cluster\":null,\"scheme\":\"http\"}]";
stubFor(get("/apps/spring-cloud-kubernetes-configserver").willReturn(
aResponse().withStatus(200).withBody(APPS_NAME).withHeader("content-type", "application/json")));
Environment environment = new Environment("test", "default");
Map<String, Object> properties = new HashMap<>();
properties.put("hello", "world");
PropertySource p = new PropertySource("p1", properties);
environment.add(p);
ObjectMapper objectMapper = new ObjectMapper();
stubFor(get("/application/default")
.willReturn(aResponse().withStatus(200).withBody(objectMapper.writeValueAsString(environment))
.withHeader("content-type", "application/json")));
}
@Test
void testBootstrapper() {
this.context = setup().run();
verify(1, getRequestedFor(urlEqualTo("/apps/spring-cloud-kubernetes-configserver")));
assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world");
}
SpringApplicationBuilder setup(String... env) {
SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class)
.properties(addDefaultEnv(env));
builder.addBootstrapRegistryInitializer(new ConfigServerBootstrapper());
return builder;
}
private String[] addDefaultEnv(String[] env) {
Set<String> set = new LinkedHashSet<>();
if (env != null && env.length > 0) {
set.addAll(Arrays.asList(env));
}
set.add("spring.cloud.config.discovery.enabled=true");
set.add("spring.config.import=optional:configserver:");
set.add("spring.cloud.config.discovery.service-id=spring-cloud-kubernetes-configserver");
set.add("spring.cloud.kubernetes.discovery.discoveryServerUrl=" + wireMockServer.baseUrl());
return set.toArray(new String[0]);
}
@SpringBootConfiguration
@EnableAutoConfiguration
static class TestConfig {
}
}

View File

@@ -60,6 +60,11 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
<optional>true</optional>
</dependency>
<!-- Testing Dependencies -->
<dependency>
@@ -117,13 +122,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.fabric8.discovery;
import java.util.Collections;
import java.util.List;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.apache.commons.logging.Log;
import org.springframework.boot.BootstrapContext;
import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.config.client.ConfigServerInstanceProvider;
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper;
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider;
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
import org.springframework.cloud.kubernetes.fabric8.Fabric8AutoConfiguration;
import org.springframework.util.ClassUtils;
/**
* @author Ryan Baxter
*/
class Fabric8ConfigServerBootstrapper extends KubernetesConfigServerBootstrapper {
@Override
public void initialize(BootstrapRegistry registry) {
if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) {
return;
}
// We need to pass a lambda here rather than create a new instance of
// ConfigServerInstanceProvider.Function
// or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on
// the classpath
registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create);
}
final static class KubernetesFunction implements ConfigServerInstanceProvider.Function {
private final BootstrapContext context;
private KubernetesFunction(BootstrapContext context) {
this.context = context;
}
static KubernetesFunction create(BootstrapContext context) {
return new KubernetesFunction(context);
}
@Override
public List<ServiceInstance> apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) {
if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) {
// If we don't have the Binder or BinderHandler from the
// ConfigDataLocationResolverContext
// we won't be able to create the necessary configuration
// properties to configure the
// Kubernetes DiscoveryClient
return Collections.emptyList();
}
KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder,
bindHandler);
KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler);
return getInstanceProvider(discoveryProperties, clientProperties, context).getInstances(serviceId);
}
private KubernetesConfigServerInstanceProvider getInstanceProvider(
KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties,
BootstrapContext context) {
if (context.isRegistered(KubernetesDiscoveryClient.class)) {
KubernetesDiscoveryClient client = context.get(KubernetesDiscoveryClient.class);
return client::getInstances;
}
else {
Fabric8AutoConfiguration fabric8AutoConfiguration = new Fabric8AutoConfiguration();
Config config = fabric8AutoConfiguration.kubernetesClientConfig(clientProperties);
KubernetesClient kubernetesClient = fabric8AutoConfiguration.kubernetesClient(config);
KubernetesDiscoveryClientAutoConfiguration discoveryClientAutoConfiguration = new KubernetesDiscoveryClientAutoConfiguration();
KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(kubernetesClient,
discoveryProperties, discoveryClientAutoConfiguration.servicesFunction(discoveryProperties),
new ServicePortSecureResolver(discoveryProperties));
return discoveryClient::getInstances;
}
}
// This method should never be called, but is there for backward
// compatibility purposes
@Override
public List<ServiceInstance> apply(String serviceId) {
return apply(serviceId, null, null, null);
}
}
}

View File

@@ -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

View File

@@ -0,0 +1,142 @@
/*
* Copyright 2013-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.kubernetes.fabric8.discovery;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.fabric8.kubernetes.api.model.Endpoints;
import io.fabric8.kubernetes.api.model.EndpointsBuilder;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServiceBuilder;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.context.ConfigurableApplicationContext;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
/**
* @author Ryan Baxter
*/
@EnableKubernetesMockClient(crud = true, https = false)
class Fabric8ConfigServerBootstrapperTests {
private static WireMockServer wireMockServer;
private KubernetesClient mockClient;
private ConfigurableApplicationContext context;
@BeforeEach
public void before() throws JsonProcessingException {
wireMockServer = new WireMockServer(options().dynamicPort());
wireMockServer.start();
WireMock.configureFor(wireMockServer.port());
// Configure the kubernetes master url to point to the mock server
System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl());
System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true");
System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");
System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false");
System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true");
Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("spring-cloud-kubernetes-configserver")
.withNamespace("test").withLabels(new HashMap<>()).endMetadata().addNewSubset().addNewAddress()
.withHostname("localhost").withIp("localhost").withNewTargetRef().withUid("10").endTargetRef()
.endAddress().addNewPort("http", "http_tcp", wireMockServer.port(), "TCP").endSubset().build();
mockClient.endpoints().inNamespace("test").create(endPoint);
Service service = new ServiceBuilder().withNewMetadata().withName("spring-cloud-kubernetes-configserver")
.withNamespace("test").withLabels(new HashMap<>()).endMetadata().build();
mockClient.services().inNamespace("test").create(service);
Environment environment = new Environment("test", "default");
Map<String, Object> properties = new HashMap<>();
properties.put("hello", "world");
org.springframework.cloud.config.environment.PropertySource p = new PropertySource("p1", properties);
environment.add(p);
ObjectMapper objectMapper = new ObjectMapper();
stubFor(get("/application/default")
.willReturn(aResponse().withStatus(200).withBody(objectMapper.writeValueAsString(environment))
.withHeader("content-type", "application/json")));
}
@AfterEach
public void after() {
wireMockServer.stop();
mockClient.close();
context.close();
}
@Test
void testBootstrapper() {
this.context = setup().run();
verify(getRequestedFor(urlEqualTo("/application/default")));
assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world");
}
SpringApplicationBuilder setup(String... env) {
SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class)
.properties(addDefaultEnv(env));
builder.addBootstrapRegistryInitializer(new Fabric8ConfigServerBootstrapper());
return builder;
}
private String[] addDefaultEnv(String[] env) {
Set<String> set = new LinkedHashSet<>();
if (env != null && env.length > 0) {
set.addAll(Arrays.asList(env));
}
set.add("spring.cloud.config.discovery.enabled=true");
set.add("spring.config.import=optional:configserver:");
set.add("spring.cloud.config.discovery.service-id=spring-cloud-kubernetes-configserver");
return set.toArray(new String[0]);
}
@SpringBootConfiguration
@EnableAutoConfiguration
static class TestConfig {
}
}