diff --git a/spring-cloud-kubernetes-client-discovery/pom.xml b/spring-cloud-kubernetes-client-discovery/pom.xml index 0a1a1f7e..4ccc540f 100644 --- a/spring-cloud-kubernetes-client-discovery/pom.xml +++ b/spring-cloud-kubernetes-client-discovery/pom.xml @@ -38,6 +38,11 @@ spring-boot-starter-webflux true + + org.springframework.cloud + spring-cloud-config-client + true + @@ -55,11 +60,6 @@ spring-boot-starter-web test - - org.springframework.cloud - spring-cloud-config-client - test - io.projectreactor reactor-test diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapper.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapper.java new file mode 100644 index 00000000..cca70701 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapper.java @@ -0,0 +1,171 @@ +/* + * Copyright 2019-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.discovery; + +import java.util.Collections; +import java.util.List; + +import io.kubernetes.client.informer.SharedIndexInformer; +import io.kubernetes.client.informer.SharedInformerFactory; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsList; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.util.Namespaces; +import io.kubernetes.client.util.generic.GenericKubernetesApi; +import org.apache.commons.logging.Log; + +import org.springframework.boot.BootstrapContext; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.config.client.ConfigServerInstanceProvider; +import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration; +import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper; +import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.util.ClassUtils; + +/** + * @author Ryan Baxter + */ +class KubernetesClientConfigServerBootstrapper extends KubernetesConfigServerBootstrapper { + + @Override + public void initialize(BootstrapRegistry registry) { + if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) { + return; + } + // We need to pass a lambda here rather than create a new instance of + // ConfigServerInstanceProvider.Function + // or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on + // the classpath + registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create); + } + + final static class KubernetesFunction implements ConfigServerInstanceProvider.Function { + + private final BootstrapContext context; + + private KubernetesFunction(BootstrapContext context) { + this.context = context; + } + + static KubernetesFunction create(BootstrapContext context) { + return new KubernetesFunction(context); + } + + @Override + public List apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) { + if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) { + // If we don't have the Binder or BinderHandler from the + // ConfigDataLocationResolverContext + // we won't be able to create the necessary configuration + // properties to configure the + // Kubernetes DiscoveryClient + return Collections.emptyList(); + } + KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder, + bindHandler); + KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler); + return getInstanceProvider(discoveryProperties, clientProperties, context, binder, bindHandler, log) + .getInstances(serviceId); + } + + protected KubernetesConfigServerInstanceProvider getInstanceProvider( + KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties, + BootstrapContext context, Binder binder, BindHandler bindHandler, Log log) { + if (context.isRegistered(KubernetesInformerDiscoveryClient.class)) { + KubernetesInformerDiscoveryClient client = context.get(KubernetesInformerDiscoveryClient.class); + return client::getInstances; + } + else { + KubernetesClientAutoConfiguration clientAutoConfiguration = new KubernetesClientAutoConfiguration(); + ApiClient apiClient = context.getOrElseSupply(ApiClient.class, + () -> clientAutoConfiguration.apiClient(clientProperties)); + + KubernetesNamespaceProvider kubernetesNamespaceProvider = clientAutoConfiguration + .kubernetesNamespaceProvider(getNamespaceEnvironment(binder, bindHandler)); + + String namespace = getInformerNamespace(kubernetesNamespaceProvider, discoveryProperties); + SharedInformerFactory sharedInformerFactory = new SharedInformerFactory(apiClient); + SpringCloudKubernetesInformerFactoryProcessor informerFactoryProcessor = new SpringCloudKubernetesInformerFactoryProcessor( + kubernetesNamespaceProvider, apiClient, sharedInformerFactory, + discoveryProperties.isAllNamespaces()); + final GenericKubernetesApi servicesApi = new GenericKubernetesApi<>( + V1Service.class, V1ServiceList.class, "", "v1", "services", apiClient); + SharedIndexInformer serviceSharedIndexInformer = sharedInformerFactory + .sharedIndexInformerFor(servicesApi, V1Service.class, 0L, namespace); + Lister serviceLister = new Lister<>(serviceSharedIndexInformer.getIndexer()); + final GenericKubernetesApi endpointsApi = new GenericKubernetesApi<>( + V1Endpoints.class, V1EndpointsList.class, "", "v1", "endpoints", apiClient); + SharedIndexInformer endpointsSharedIndexInformer = sharedInformerFactory + .sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0L, namespace); + Lister endpointsLister = new Lister<>(endpointsSharedIndexInformer.getIndexer()); + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( + kubernetesNamespaceProvider.getNamespace(), sharedInformerFactory, serviceLister, + endpointsLister, serviceSharedIndexInformer, endpointsSharedIndexInformer, discoveryProperties); + try { + discoveryClient.afterPropertiesSet(); + return discoveryClient::getInstances; + } + catch (Exception e) { + if (log != null) { + log.warn("Error initiating informer discovery client", e); + } + return (serviceId) -> Collections.emptyList(); + } + finally { + sharedInformerFactory.stopAllRegisteredInformers(); + } + } + } + + private String getInformerNamespace(KubernetesNamespaceProvider kubernetesNamespaceProvider, + KubernetesDiscoveryProperties discoveryProperties) { + return discoveryProperties.isAllNamespaces() ? Namespaces.NAMESPACE_ALL + : kubernetesNamespaceProvider.getNamespace() == null ? Namespaces.NAMESPACE_DEFAULT + : kubernetesNamespaceProvider.getNamespace(); + } + + private Environment getNamespaceEnvironment(Binder binder, BindHandler bindHandler) { + return new AbstractEnvironment() { + @Override + public String getProperty(String key) { + return binder.bind(key, Bindable.of(String.class), bindHandler).orElse(super.getProperty(key)); + } + }; + } + + // This method should never be called, but is there for backward + // compatibility purposes + @Override + public List apply(String serviceId) { + return apply(serviceId, null, null, null); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories index 507576df..8e4f0054 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories @@ -5,3 +5,6 @@ org.springframework.cloud.kubernetes.client.discovery.reactive.KubernetesInforme org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientConfigClientBootstrapConfiguration +org.springframework.boot.BootstrapRegistryInitializer=\ +org.springframework.cloud.kubernetes.client.discovery.KubernetesClientConfigServerBootstrapper + diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapperTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapperTests.java new file mode 100644 index 00000000..56057e01 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapperTests.java @@ -0,0 +1,177 @@ +/* + * Copyright 2019-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.discovery; + +import java.time.Duration; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointPort; +import io.kubernetes.client.openapi.models.V1EndpointSubset; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsList; +import io.kubernetes.client.openapi.models.V1EndpointsListBuilder; +import io.kubernetes.client.openapi.models.V1ListMetaBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1ObjectReferenceBuilder; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.openapi.models.V1ServiceListBuilder; +import io.kubernetes.client.openapi.models.V1ServicePortBuilder; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * @author Ryan Baxter + */ +class KubernetesClientConfigServerBootstrapperTests { + + private static WireMockServer wireMockServer; + + private ConfigurableApplicationContext context; + + @BeforeEach + public void before() throws JsonProcessingException { + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor(wireMockServer.port()); + + V1ServiceList SERVICE_LIST = new V1ServiceListBuilder() + .withMetadata(new V1ListMetaBuilder().withResourceVersion("1").build()) + .addToItems(new V1ServiceBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("spring-cloud-kubernetes-configserver") + .withNamespace("default").withResourceVersion("0").addToLabels("beta", "true") + .addToAnnotations("org.springframework.cloud", "true").withUid("0").build()) + .withSpec(new V1ServiceSpecBuilder().withClusterIP("localhost").withSessionAffinity("None") + .withType("ClusterIP") + .addToPorts(new V1ServicePortBuilder().withPort(wireMockServer.port()).withName("http") + .withProtocol("TCP").withNewTargetPort(wireMockServer.port()).build()) + .build()) + .build()) + .build(); + + V1EndpointsList ENDPOINTS_LIST = new V1EndpointsListBuilder() + .withMetadata(new V1ListMetaBuilder().withResourceVersion("0").build()) + .addToItems(new V1Endpoints() + .metadata(new V1ObjectMeta().name("spring-cloud-kubernetes-configserver").namespace("default")) + .addSubsetsItem( + new V1EndpointSubset() + .addPortsItem(new V1EndpointPort().port(wireMockServer.port()).name("http")) + .addAddressesItem(new V1EndpointAddress().hostname("localhost").ip("localhost") + .targetRef(new V1ObjectReferenceBuilder().withUid("uid1").build())))) + .build(); + + Environment environment = new Environment("test", "default"); + Map properties = new HashMap<>(); + properties.put("hello", "world"); + org.springframework.cloud.config.environment.PropertySource p = new PropertySource("p1", properties); + environment.add(p); + ObjectMapper objectMapper = new ObjectMapper(); + stubFor(get("/application/default") + .willReturn(aResponse().withStatus(200).withBody(objectMapper.writeValueAsString(environment)) + .withHeader("content-type", "application/json"))); + stubFor(get("/api/v1/namespaces/default/endpoints?resourceVersion=0&watch=false") + .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(ENDPOINTS_LIST)) + .withHeader("content-type", "application/json"))); + stubFor(get("/api/v1/namespaces/default/services?resourceVersion=0&watch=false") + .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SERVICE_LIST)) + .withHeader("content-type", "application/json"))); + stubFor(get(urlMatching("/api/v1/namespaces/default/services.*.watch=true")) + .willReturn(aResponse().withStatus(200))); + stubFor(get(urlMatching("/api/v1/namespaces/default/endpoints.*.watch=true")) + .willReturn(aResponse().withStatus(200))); + } + + @AfterEach + public void after() { + wireMockServer.stop(); + context.close(); + } + + @Test + void testBootstrapper() { + this.context = setup().run(); + verify(getRequestedFor(urlEqualTo("/application/default"))); + assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world"); + } + + SpringApplicationBuilder setup(String... env) { + SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class) + .properties(addDefaultEnv(env)); + ApiClient apiClient = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()) + .setReadTimeout(Duration.ZERO).build(); + builder.addBootstrapRegistryInitializer(registry -> registry.register(ApiClient.class, (context) -> apiClient)); + builder.addBootstrapRegistryInitializer(new KubernetesClientConfigServerBootstrapper()); + return builder; + } + + private String[] addDefaultEnv(String[] env) { + Set set = new LinkedHashSet<>(); + if (env != null && env.length > 0) { + set.addAll(Arrays.asList(env)); + } + set.add("spring.cloud.config.discovery.enabled=true"); + set.add("spring.config.import=optional:configserver:"); + set.add("spring.cloud.config.discovery.service-id=spring-cloud-kubernetes-configserver"); + set.add("spring.cloud.kubernetes.client.namespace=default"); + return set.toArray(new String[0]); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + static class TestConfig { + + @Bean + public ApiClient apiClient() { + return new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + } + + } + +} diff --git a/spring-cloud-kubernetes-commons/pom.xml b/spring-cloud-kubernetes-commons/pom.xml index 6d020f04..70b4116d 100644 --- a/spring-cloud-kubernetes-commons/pom.xml +++ b/spring-cloud-kubernetes-commons/pom.xml @@ -67,6 +67,11 @@ spring-boot-starter-aop true + + org.springframework.cloud + spring-cloud-config-client + true + org.springframework.boot diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerBootstrapper.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerBootstrapper.java new file mode 100644 index 00000000..de6d9049 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerBootstrapper.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.config; + +import org.springframework.boot.BootstrapRegistryInitializer; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.config.client.ConfigClientProperties; +import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.util.ClassUtils; + +/** + * @author Ryan Baxter + */ +public abstract class KubernetesConfigServerBootstrapper implements BootstrapRegistryInitializer { + + public static boolean hasConfigServerInstanceProvider() { + return !ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null); + } + + public static KubernetesDiscoveryProperties createKubernetesDiscoveryProperties(Binder binder, + BindHandler bindHandler) { + return binder.bind("spring.cloud.kubernetes.discovery", Bindable.of(KubernetesDiscoveryProperties.class), + bindHandler).orElseGet(KubernetesDiscoveryProperties::new); + } + + public static KubernetesClientProperties createKubernetesClientProperties(Binder binder, BindHandler bindHandler) { + return binder.bind("spring.cloud.kubernetes.client", Bindable.of(KubernetesClientProperties.class), bindHandler) + .orElseGet(KubernetesClientProperties::new); + } + + public static Boolean getDiscoveryEnabled(Binder binder, BindHandler bindHandler) { + return binder.bind(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED, Bindable.of(Boolean.class), bindHandler) + .orElse(false); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerInstanceProvider.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerInstanceProvider.java new file mode 100644 index 00000000..c71e78a8 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerInstanceProvider.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.config; + +import java.util.List; + +import org.springframework.cloud.client.ServiceInstance; + +/** + * @author Ryan Baxter + */ +public interface KubernetesConfigServerInstanceProvider { + + List getInstances(String serviceId); + +} diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsAutoConfigurationTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsAutoConfigurationTests.java index fca088cf..c0b90a22 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsAutoConfigurationTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsAutoConfigurationTests.java @@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = KubernetesCommonsAutoConfigurationTests.App.class, properties = { "spring.cloud.kubernetes.client.password=mypassword", - "spring.cloud.kubernetes.client.proxy-password=myproxypassword" }) + "spring.cloud.kubernetes.client.proxy-password=myproxypassword", "spring.cloud.config.enabled=false" }) public class KubernetesCommonsAutoConfigurationTests { @Autowired diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesBootstrapConfigurationTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesBootstrapConfigurationTests.java index 1d41e7f0..7356c295 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesBootstrapConfigurationTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/KubernetesBootstrapConfigurationTests.java @@ -42,7 +42,13 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class KubernetesBootstrapConfigurationTests { - @SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class) + @SpringBootApplication + static class App { + + } + + @SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class, + properties = { "spring.cloud.config.enabled=false" }) @Nested public class FailFastDisabled { @@ -57,7 +63,7 @@ public class KubernetesBootstrapConfigurationTests { } @SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class, - properties = { "spring.cloud.kubernetes.config.fail-fast=true" }) + properties = { "spring.cloud.kubernetes.config.fail-fast=true", "spring.cloud.config.enabled=false" }) @Nested public class ConfigFailFastEnabled { @@ -89,7 +95,7 @@ public class KubernetesBootstrapConfigurationTests { } @SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class, - properties = { "spring.cloud.kubernetes.secrets.fail-fast=true" }) + properties = { "spring.cloud.kubernetes.secrets.fail-fast=true", "spring.cloud.config.enabled=false" }) @Nested public class SecretsFailFastEnabled { @@ -120,8 +126,9 @@ public class KubernetesBootstrapConfigurationTests { } - @SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class, properties = { - "spring.cloud.kubernetes.config.fail-fast=true", "spring.cloud.kubernetes.secrets.fail-fast=true" }) + @SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class, + properties = { "spring.cloud.kubernetes.config.fail-fast=true", + "spring.cloud.kubernetes.secrets.fail-fast=true", "spring.cloud.config.enabled=false" }) @Nested public class ConfigAndSecretsFailFastEnabledWithDefaultRetryConfiguration { @@ -170,7 +177,7 @@ public class KubernetesBootstrapConfigurationTests { "spring.cloud.kubernetes.config.retry.max-attempts=3", "spring.cloud.kubernetes.config.retry.initial-interval=1500", "spring.cloud.kubernetes.config.retry.max-interval=3000", - "spring.cloud.kubernetes.config.retry.multiplier=1.5" }) + "spring.cloud.kubernetes.config.retry.multiplier=1.5", "spring.cloud.config.enabled=false" }) @Nested public class ConfigFailFastEnabledWithCustomRetryConfiguration { @@ -194,7 +201,7 @@ public class KubernetesBootstrapConfigurationTests { "spring.cloud.kubernetes.secrets.retry.max-attempts=3", "spring.cloud.kubernetes.secrets.retry.initial-interval=1500", "spring.cloud.kubernetes.secrets.retry.max-interval=3000", - "spring.cloud.kubernetes.secrets.retry.multiplier=1.5" }) + "spring.cloud.kubernetes.secrets.retry.multiplier=1.5", "spring.cloud.config.enabled=false" }) @Nested public class SecretsFailFastEnabledWithCustomRetryConfiguration { @@ -213,8 +220,9 @@ public class KubernetesBootstrapConfigurationTests { } - @SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class, properties = { - "spring.cloud.kubernetes.config.fail-fast=true", "spring.cloud.kubernetes.config.retry.enabled=false" }) + @SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class, + properties = { "spring.cloud.kubernetes.config.fail-fast=true", + "spring.cloud.kubernetes.config.retry.enabled=false", "spring.cloud.config.enabled=false" }) @Nested public class ConfigFailFastEnabledButRetryDisabled { @@ -228,8 +236,9 @@ public class KubernetesBootstrapConfigurationTests { } - @SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class, properties = { - "spring.cloud.kubernetes.secrets.fail-fast=true", "spring.cloud.kubernetes.secrets.retry.enabled=false" }) + @SpringBootTest(webEnvironment = WebEnvironment.NONE, classes = App.class, + properties = { "spring.cloud.kubernetes.secrets.fail-fast=true", + "spring.cloud.kubernetes.secrets.retry.enabled=false", "spring.cloud.config.enabled=false" }) @Nested public class SecretsFailFastEnabledButRetryDisabled { @@ -272,9 +281,4 @@ public class KubernetesBootstrapConfigurationTests { } - @SpringBootApplication - static class App { - - } - } diff --git a/spring-cloud-kubernetes-discovery/pom.xml b/spring-cloud-kubernetes-discovery/pom.xml index 19ae635d..8262de42 100644 --- a/spring-cloud-kubernetes-discovery/pom.xml +++ b/spring-cloud-kubernetes-discovery/pom.xml @@ -24,6 +24,15 @@ org.springframework.cloud spring-cloud-commons + + org.springframework.cloud + spring-cloud-kubernetes-commons + + + org.springframework.cloud + spring-cloud-config-client + true + org.springframework.boot spring-boot-actuator diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapper.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapper.java new file mode 100644 index 00000000..a68d89b8 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapper.java @@ -0,0 +1,106 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.Log; + +import org.springframework.boot.BootstrapContext; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.config.client.ConfigServerInstanceProvider; +import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; +import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper; +import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.util.ClassUtils; + +/** + * @author Ryan Baxter + */ +class ConfigServerBootstrapper extends KubernetesConfigServerBootstrapper { + + @Override + public void initialize(BootstrapRegistry registry) { + if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) { + return; + } + // We need to pass a lambda here rather than create a new instance of + // ConfigServerInstanceProvider.Function + // or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on + // the classpath + registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create); + } + + final static class KubernetesFunction implements ConfigServerInstanceProvider.Function { + + private final BootstrapContext context; + + private KubernetesFunction(BootstrapContext context) { + this.context = context; + } + + static KubernetesFunction create(BootstrapContext context) { + return new KubernetesFunction(context); + } + + @Override + public List apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) { + if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) { + // If we don't have the Binder or BinderHandler from the + // ConfigDataLocationResolverContext + // we won't be able to create the necessary configuration + // properties to configure the + // Kubernetes DiscoveryClient + return Collections.emptyList(); + } + KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder, + bindHandler); + KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler); + return getInstanceProvider(discoveryProperties, clientProperties, context, binder, bindHandler, log) + .getInstances(serviceId); + } + + private KubernetesConfigServerInstanceProvider getInstanceProvider( + KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties, + BootstrapContext context, Binder binder, BindHandler bindHandler, Log log) { + KubernetesDiscoveryClientProperties kubernetesDiscoveryClientProperties = binder + .bind("spring.cloud.kubernetes.discovery", Bindable.of(KubernetesDiscoveryClientProperties.class), + bindHandler) + .orElseGet(KubernetesDiscoveryClientProperties::new); + KubernetesDiscoveryClientAutoConfiguration.Servlet autoConfiguration = new KubernetesDiscoveryClientAutoConfiguration.Servlet(); + DiscoveryClient discoveryClient = autoConfiguration + .kubernetesDiscoveryClient(autoConfiguration.restTemplate(), kubernetesDiscoveryClientProperties); + return discoveryClient::getInstances; + } + + // This method should never be called, but is there for backward + // compatibility purposes + @Override + public List apply(String serviceId) { + return apply(serviceId, null, null, null); + } + + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories index a82e049f..7cb0c2ab 100644 --- a/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories @@ -1,2 +1,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClientAutoConfiguration + +org.springframework.boot.BootstrapRegistryInitializer=\ +org.springframework.cloud.kubernetes.discovery.ConfigServerBootstrapper diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapperTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapperTests.java new file mode 100644 index 00000000..bf1d47e0 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapperTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.context.ConfigurableApplicationContext; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ryan Baxter + */ +class ConfigServerBootstrapperTests { + + private static WireMockServer wireMockServer; + + protected ConfigurableApplicationContext context; + + @AfterEach + void close() { + wireMockServer.stop(); + if (this.context != null) { + this.context.close(); + } + } + + @BeforeEach + void beforeAll() throws JsonProcessingException { + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor(wireMockServer.port()); + String APPS_NAME = "[{\"instanceId\":\"uid2\",\"serviceId\":\"spring-cloud-kubernetes-configserver\",\"host\":\"localhost\",\"port\":" + + wireMockServer.port() + ",\"uri\":\"" + wireMockServer.baseUrl() + + "\",\"secure\":false,\"metadata\":{\"spring\":\"true\",\"http\":\"8080\",\"k8s\":\"true\"},\"namespace\":\"namespace1\",\"cluster\":null,\"scheme\":\"http\"}]"; + stubFor(get("/apps/spring-cloud-kubernetes-configserver").willReturn( + aResponse().withStatus(200).withBody(APPS_NAME).withHeader("content-type", "application/json"))); + Environment environment = new Environment("test", "default"); + Map properties = new HashMap<>(); + properties.put("hello", "world"); + PropertySource p = new PropertySource("p1", properties); + environment.add(p); + ObjectMapper objectMapper = new ObjectMapper(); + stubFor(get("/application/default") + .willReturn(aResponse().withStatus(200).withBody(objectMapper.writeValueAsString(environment)) + .withHeader("content-type", "application/json"))); + } + + @Test + void testBootstrapper() { + this.context = setup().run(); + verify(1, getRequestedFor(urlEqualTo("/apps/spring-cloud-kubernetes-configserver"))); + assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world"); + } + + SpringApplicationBuilder setup(String... env) { + SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class) + .properties(addDefaultEnv(env)); + builder.addBootstrapRegistryInitializer(new ConfigServerBootstrapper()); + return builder; + } + + private String[] addDefaultEnv(String[] env) { + Set set = new LinkedHashSet<>(); + if (env != null && env.length > 0) { + set.addAll(Arrays.asList(env)); + } + set.add("spring.cloud.config.discovery.enabled=true"); + set.add("spring.config.import=optional:configserver:"); + set.add("spring.cloud.config.discovery.service-id=spring-cloud-kubernetes-configserver"); + set.add("spring.cloud.kubernetes.discovery.discoveryServerUrl=" + wireMockServer.baseUrl()); + return set.toArray(new String[0]); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + static class TestConfig { + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-discovery/pom.xml b/spring-cloud-kubernetes-fabric8-discovery/pom.xml index 1ac11a80..4ee3d228 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/pom.xml +++ b/spring-cloud-kubernetes-fabric8-discovery/pom.xml @@ -60,6 +60,11 @@ org.springframework.cloud spring-cloud-context + + org.springframework.cloud + spring-cloud-config-client + true + @@ -117,13 +122,13 @@ test - org.springframework.cloud - spring-cloud-config-client + io.projectreactor + reactor-test test - io.projectreactor - reactor-test + com.github.tomakehurst + wiremock-jre8 test diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapper.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapper.java new file mode 100644 index 00000000..067762e5 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapper.java @@ -0,0 +1,112 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.discovery; + +import java.util.Collections; +import java.util.List; + +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.apache.commons.logging.Log; + +import org.springframework.boot.BootstrapContext; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.config.client.ConfigServerInstanceProvider; +import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; +import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper; +import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.fabric8.Fabric8AutoConfiguration; +import org.springframework.util.ClassUtils; + +/** + * @author Ryan Baxter + */ +class Fabric8ConfigServerBootstrapper extends KubernetesConfigServerBootstrapper { + + @Override + public void initialize(BootstrapRegistry registry) { + if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) { + return; + } + // We need to pass a lambda here rather than create a new instance of + // ConfigServerInstanceProvider.Function + // or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on + // the classpath + registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create); + } + + final static class KubernetesFunction implements ConfigServerInstanceProvider.Function { + + private final BootstrapContext context; + + private KubernetesFunction(BootstrapContext context) { + this.context = context; + } + + static KubernetesFunction create(BootstrapContext context) { + return new KubernetesFunction(context); + } + + @Override + public List apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) { + if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) { + // If we don't have the Binder or BinderHandler from the + // ConfigDataLocationResolverContext + // we won't be able to create the necessary configuration + // properties to configure the + // Kubernetes DiscoveryClient + return Collections.emptyList(); + } + KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder, + bindHandler); + KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler); + return getInstanceProvider(discoveryProperties, clientProperties, context).getInstances(serviceId); + } + + private KubernetesConfigServerInstanceProvider getInstanceProvider( + KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties, + BootstrapContext context) { + if (context.isRegistered(KubernetesDiscoveryClient.class)) { + KubernetesDiscoveryClient client = context.get(KubernetesDiscoveryClient.class); + return client::getInstances; + } + else { + Fabric8AutoConfiguration fabric8AutoConfiguration = new Fabric8AutoConfiguration(); + Config config = fabric8AutoConfiguration.kubernetesClientConfig(clientProperties); + KubernetesClient kubernetesClient = fabric8AutoConfiguration.kubernetesClient(config); + KubernetesDiscoveryClientAutoConfiguration discoveryClientAutoConfiguration = new KubernetesDiscoveryClientAutoConfiguration(); + KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(kubernetesClient, + discoveryProperties, discoveryClientAutoConfiguration.servicesFunction(discoveryProperties), + new ServicePortSecureResolver(discoveryProperties)); + return discoveryClient::getInstances; + } + } + + // This method should never be called, but is there for backward + // compatibility purposes + @Override + public List apply(String serviceId) { + return apply(serviceId, null, null, null); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-fabric8-discovery/src/main/resources/META-INF/spring.factories index 4746c884..ebe17137 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/resources/META-INF/spring.factories @@ -4,3 +4,5 @@ org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClient org.springframework.cloud.kubernetes.fabric8.discovery.reactive.KubernetesReactiveDiscoveryClientAutoConfiguration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClientConfigClientBootstrapConfiguration +org.springframework.boot.BootstrapRegistryInitializer=\ +org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8ConfigServerBootstrapper diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapperTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapperTests.java new file mode 100644 index 00000000..e9e66de3 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapperTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.discovery; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.fabric8.kubernetes.api.model.Endpoints; +import io.fabric8.kubernetes.api.model.EndpointsBuilder; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.context.ConfigurableApplicationContext; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * @author Ryan Baxter + */ +@EnableKubernetesMockClient(crud = true, https = false) +class Fabric8ConfigServerBootstrapperTests { + + private static WireMockServer wireMockServer; + + private KubernetesClient mockClient; + + private ConfigurableApplicationContext context; + + @BeforeEach + public void before() throws JsonProcessingException { + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor(wireMockServer.port()); + + // Configure the kubernetes master url to point to the mock server + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + + Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("spring-cloud-kubernetes-configserver") + .withNamespace("test").withLabels(new HashMap<>()).endMetadata().addNewSubset().addNewAddress() + .withHostname("localhost").withIp("localhost").withNewTargetRef().withUid("10").endTargetRef() + .endAddress().addNewPort("http", "http_tcp", wireMockServer.port(), "TCP").endSubset().build(); + + mockClient.endpoints().inNamespace("test").create(endPoint); + + Service service = new ServiceBuilder().withNewMetadata().withName("spring-cloud-kubernetes-configserver") + .withNamespace("test").withLabels(new HashMap<>()).endMetadata().build(); + + mockClient.services().inNamespace("test").create(service); + + Environment environment = new Environment("test", "default"); + Map properties = new HashMap<>(); + properties.put("hello", "world"); + org.springframework.cloud.config.environment.PropertySource p = new PropertySource("p1", properties); + environment.add(p); + ObjectMapper objectMapper = new ObjectMapper(); + stubFor(get("/application/default") + .willReturn(aResponse().withStatus(200).withBody(objectMapper.writeValueAsString(environment)) + .withHeader("content-type", "application/json"))); + } + + @AfterEach + public void after() { + wireMockServer.stop(); + mockClient.close(); + context.close(); + } + + @Test + void testBootstrapper() { + this.context = setup().run(); + verify(getRequestedFor(urlEqualTo("/application/default"))); + assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world"); + } + + SpringApplicationBuilder setup(String... env) { + SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class) + .properties(addDefaultEnv(env)); + builder.addBootstrapRegistryInitializer(new Fabric8ConfigServerBootstrapper()); + return builder; + } + + private String[] addDefaultEnv(String[] env) { + Set set = new LinkedHashSet<>(); + if (env != null && env.length > 0) { + set.addAll(Arrays.asList(env)); + } + set.add("spring.cloud.config.discovery.enabled=true"); + set.add("spring.config.import=optional:configserver:"); + set.add("spring.cloud.config.discovery.service-id=spring-cloud-kubernetes-configserver"); + return set.toArray(new String[0]); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + static class TestConfig { + + } + +}