Merge branch '3.0.x'
This commit is contained in:
@@ -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') }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-config-client</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing Dependencies -->
|
||||
<dependency>
|
||||
@@ -55,11 +60,6 @@
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-config-client</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
|
||||
@@ -0,0 +1,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<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 {
|
||||
|
||||
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<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(
|
||||
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<ServiceInstance> apply(String serviceId) {
|
||||
return apply(serviceId, null, null, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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<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("server.port=0");
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -75,6 +75,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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,6 +21,15 @@
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-commons</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-kubernetes-commons</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-config-client</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-actuator</artifactId>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2013-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.kubernetes.discovery;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
|
||||
import org.springframework.boot.BootstrapContext;
|
||||
import org.springframework.boot.BootstrapRegistry;
|
||||
import org.springframework.boot.context.properties.bind.BindHandler;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.config.client.ConfigServerInstanceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties;
|
||||
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper;
|
||||
import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider;
|
||||
import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* @author Ryan Baxter
|
||||
*/
|
||||
class ConfigServerBootstrapper extends KubernetesConfigServerBootstrapper {
|
||||
|
||||
@Override
|
||||
public void initialize(BootstrapRegistry registry) {
|
||||
if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) {
|
||||
return;
|
||||
}
|
||||
// We need to pass a lambda here rather than create a new instance of
|
||||
// ConfigServerInstanceProvider.Function
|
||||
// or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on
|
||||
// the classpath
|
||||
registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create);
|
||||
}
|
||||
|
||||
final static class KubernetesFunction implements ConfigServerInstanceProvider.Function {
|
||||
|
||||
private final BootstrapContext context;
|
||||
|
||||
private KubernetesFunction(BootstrapContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
static KubernetesFunction create(BootstrapContext context) {
|
||||
return new KubernetesFunction(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ServiceInstance> apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) {
|
||||
if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) {
|
||||
// If we don't have the Binder or BinderHandler from the
|
||||
// ConfigDataLocationResolverContext
|
||||
// we won't be able to create the necessary configuration
|
||||
// properties to configure the
|
||||
// Kubernetes DiscoveryClient
|
||||
return Collections.emptyList();
|
||||
}
|
||||
KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder,
|
||||
bindHandler);
|
||||
KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler);
|
||||
return getInstanceProvider(discoveryProperties, clientProperties, context, binder, bindHandler, log)
|
||||
.getInstances(serviceId);
|
||||
}
|
||||
|
||||
private KubernetesConfigServerInstanceProvider getInstanceProvider(
|
||||
KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties,
|
||||
BootstrapContext context, Binder binder, BindHandler bindHandler, Log log) {
|
||||
KubernetesDiscoveryClientProperties kubernetesDiscoveryClientProperties = binder
|
||||
.bind("spring.cloud.kubernetes.discovery", Bindable.of(KubernetesDiscoveryClientProperties.class),
|
||||
bindHandler)
|
||||
.orElseGet(KubernetesDiscoveryClientProperties::new);
|
||||
KubernetesDiscoveryClientAutoConfiguration.Servlet autoConfiguration = new KubernetesDiscoveryClientAutoConfiguration.Servlet();
|
||||
DiscoveryClient discoveryClient = autoConfiguration
|
||||
.kubernetesDiscoveryClient(autoConfiguration.restTemplate(), kubernetesDiscoveryClientProperties);
|
||||
return discoveryClient::getInstances;
|
||||
}
|
||||
|
||||
// This method should never be called, but is there for backward
|
||||
// compatibility purposes
|
||||
@Override
|
||||
public List<ServiceInstance> apply(String serviceId) {
|
||||
return apply(serviceId, null, null, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.BootstrapRegistryInitializer=\
|
||||
org.springframework.cloud.kubernetes.discovery.ConfigServerBootstrapper
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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("server.port=0");
|
||||
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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -42,6 +42,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>
|
||||
@@ -102,13 +107,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-standalone</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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, 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<ServiceInstance> apply(String serviceId) {
|
||||
return apply(serviceId, null, null, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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.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 wiremock.com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import wiremock.com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
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<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");
|
||||
set.add("spring.cloud.kubernetes.client.namespace=test");
|
||||
set.add("server.port=0");
|
||||
return set.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user