diff --git a/.github/workflows/composites/cache/action.yaml b/.github/workflows/composites/cache/action.yaml index b823306e..a4504fb6 100644 --- a/.github/workflows/composites/cache/action.yaml +++ b/.github/workflows/composites/cache/action.yaml @@ -3,7 +3,7 @@ description: cache runs: using: "composite" steps: - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: ~/.m2/repository key: ${{ runner.os }}-cache-${{ env.BRANCH_NAME }}-${{ hashFiles('**/pom.xml') }} diff --git a/.github/workflows/composites/download-docker-images/action.yaml b/.github/workflows/composites/download-docker-images/action.yaml index 038d2c65..0a26162e 100644 --- a/.github/workflows/composites/download-docker-images/action.yaml +++ b/.github/workflows/composites/download-docker-images/action.yaml @@ -6,7 +6,7 @@ description: download docker images runs: using: "composite" steps: - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: /tmp/docker/images key: docker-images-cache-${{ env.DOCKER_IMAGES_KEY }} diff --git a/.github/workflows/composites/setup-jdk1.8/action.yaml b/.github/workflows/composites/setup-jdk1.8/action.yaml index 900a3a19..7dab6375 100755 --- a/.github/workflows/composites/setup-jdk1.8/action.yaml +++ b/.github/workflows/composites/setup-jdk1.8/action.yaml @@ -3,7 +3,7 @@ description: setup project with jdk-8 runs: using: "composite" steps: - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: '8' diff --git a/.github/workflows/composites/setup-jdk17/action.yaml b/.github/workflows/composites/setup-jdk17/action.yaml index 776a68df..19dcd374 100755 --- a/.github/workflows/composites/setup-jdk17/action.yaml +++ b/.github/workflows/composites/setup-jdk17/action.yaml @@ -3,7 +3,7 @@ description: setup project with jdk-17 runs: using: "composite" steps: - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: '17' diff --git a/.github/workflows/composites/upload-docker-images/action.yaml b/.github/workflows/composites/upload-docker-images/action.yaml index b0506b82..ed7bf1ea 100644 --- a/.github/workflows/composites/upload-docker-images/action.yaml +++ b/.github/workflows/composites/upload-docker-images/action.yaml @@ -6,7 +6,7 @@ description: upload docker images runs: using: "composite" steps: - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: /tmp/docker/images key: docker-images-cache-${{ env.DOCKER_IMAGES_KEY }} diff --git a/spring-cloud-kubernetes-client-discovery/pom.xml b/spring-cloud-kubernetes-client-discovery/pom.xml index 6501c41e..e982e0a6 100644 --- a/spring-cloud-kubernetes-client-discovery/pom.xml +++ b/spring-cloud-kubernetes-client-discovery/pom.xml @@ -38,6 +38,11 @@ spring-boot-starter-webflux true + + org.springframework.cloud + spring-cloud-config-client + true + @@ -55,11 +60,6 @@ spring-boot-starter-web test - - org.springframework.cloud - spring-cloud-config-client - test - io.projectreactor reactor-test diff --git a/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapper.java b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapper.java new file mode 100644 index 00000000..282b8438 --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/main/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapper.java @@ -0,0 +1,173 @@ +/* + * Copyright 2019-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.discovery; + +import java.util.Collections; +import java.util.List; + +import io.kubernetes.client.informer.SharedIndexInformer; +import io.kubernetes.client.informer.SharedInformerFactory; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsList; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.util.Namespaces; +import io.kubernetes.client.util.generic.GenericKubernetesApi; +import org.apache.commons.logging.Log; + +import org.springframework.boot.BootstrapContext; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.config.client.ConfigServerInstanceProvider; +import org.springframework.cloud.kubernetes.client.KubernetesClientAutoConfiguration; +import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper; +import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.util.ClassUtils; + +import static org.springframework.cloud.kubernetes.client.KubernetesClientUtils.kubernetesApiClient; + +/** + * @author Ryan Baxter + */ +class KubernetesClientConfigServerBootstrapper extends KubernetesConfigServerBootstrapper { + + @Override + public void initialize(BootstrapRegistry registry) { + if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) { + return; + } + // We need to pass a lambda here rather than create a new instance of + // ConfigServerInstanceProvider.Function + // or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on + // the classpath + registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create); + } + + final static class KubernetesFunction implements ConfigServerInstanceProvider.Function { + + private final BootstrapContext context; + + private KubernetesFunction(BootstrapContext context) { + this.context = context; + } + + static KubernetesFunction create(BootstrapContext context) { + return new KubernetesFunction(context); + } + + @Override + public List apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) { + if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) { + // If we don't have the Binder or BinderHandler from the + // ConfigDataLocationResolverContext + // we won't be able to create the necessary configuration + // properties to configure the + // Kubernetes DiscoveryClient + return Collections.emptyList(); + } + KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder, + bindHandler); + KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler); + return getInstanceProvider(discoveryProperties, clientProperties, context, binder, bindHandler, log) + .getInstances(serviceId); + } + + protected KubernetesConfigServerInstanceProvider getInstanceProvider( + KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties, + BootstrapContext context, Binder binder, BindHandler bindHandler, Log log) { + if (context.isRegistered(KubernetesInformerDiscoveryClient.class)) { + KubernetesInformerDiscoveryClient client = context.get(KubernetesInformerDiscoveryClient.class); + return client::getInstances; + } + else { + + ApiClient defaultApiClient = kubernetesApiClient(); + defaultApiClient.setUserAgent(binder.bind("spring.cloud.kubernetes.client.user-agent", String.class) + .orElse(KubernetesClientProperties.DEFAULT_USER_AGENT)); + KubernetesClientAutoConfiguration clientAutoConfiguration = new KubernetesClientAutoConfiguration(); + ApiClient apiClient = context.getOrElseSupply(ApiClient.class, () -> defaultApiClient); + + KubernetesNamespaceProvider kubernetesNamespaceProvider = clientAutoConfiguration + .kubernetesNamespaceProvider(getNamespaceEnvironment(binder, bindHandler)); + + String namespace = getInformerNamespace(kubernetesNamespaceProvider, discoveryProperties); + SharedInformerFactory sharedInformerFactory = new SharedInformerFactory(apiClient); + final GenericKubernetesApi servicesApi = new GenericKubernetesApi<>( + V1Service.class, V1ServiceList.class, "", "v1", "services", apiClient); + SharedIndexInformer serviceSharedIndexInformer = sharedInformerFactory + .sharedIndexInformerFor(servicesApi, V1Service.class, 0L, namespace); + Lister serviceLister = new Lister<>(serviceSharedIndexInformer.getIndexer()); + final GenericKubernetesApi endpointsApi = new GenericKubernetesApi<>( + V1Endpoints.class, V1EndpointsList.class, "", "v1", "endpoints", apiClient); + SharedIndexInformer endpointsSharedIndexInformer = sharedInformerFactory + .sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0L, namespace); + Lister endpointsLister = new Lister<>(endpointsSharedIndexInformer.getIndexer()); + KubernetesInformerDiscoveryClient discoveryClient = new KubernetesInformerDiscoveryClient( + sharedInformerFactory, serviceLister, endpointsLister, serviceSharedIndexInformer, + endpointsSharedIndexInformer, discoveryProperties); + try { + discoveryClient.afterPropertiesSet(); + return discoveryClient::getInstances; + } + catch (Exception e) { + if (log != null) { + log.warn("Error initiating informer discovery client", e); + } + return (serviceId) -> Collections.emptyList(); + } + finally { + sharedInformerFactory.stopAllRegisteredInformers(); + } + } + } + + private String getInformerNamespace(KubernetesNamespaceProvider kubernetesNamespaceProvider, + KubernetesDiscoveryProperties discoveryProperties) { + return discoveryProperties.allNamespaces() ? Namespaces.NAMESPACE_ALL + : kubernetesNamespaceProvider.getNamespace() == null ? Namespaces.NAMESPACE_DEFAULT + : kubernetesNamespaceProvider.getNamespace(); + } + + private Environment getNamespaceEnvironment(Binder binder, BindHandler bindHandler) { + return new AbstractEnvironment() { + @Override + public String getProperty(String key) { + return binder.bind(key, Bindable.of(String.class), bindHandler).orElse(super.getProperty(key)); + } + }; + } + + // This method should never be called, but is there for backward + // compatibility purposes + @Override + public List apply(String serviceId) { + return apply(serviceId, null, null, null); + } + + } + +} diff --git a/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories index 31842ad6..f1630371 100644 --- a/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-kubernetes-client-discovery/src/main/resources/META-INF/spring.factories @@ -1,3 +1,6 @@ org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.kubernetes.client.discovery.KubernetesDiscoveryClientConfigClientBootstrapConfiguration +org.springframework.boot.BootstrapRegistryInitializer=\ +org.springframework.cloud.kubernetes.client.discovery.KubernetesClientConfigServerBootstrapper + diff --git a/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapperTests.java b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapperTests.java new file mode 100644 index 00000000..76f895da --- /dev/null +++ b/spring-cloud-kubernetes-client-discovery/src/test/java/org/springframework/cloud/kubernetes/client/discovery/KubernetesClientConfigServerBootstrapperTests.java @@ -0,0 +1,177 @@ +/* + * Copyright 2019-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.client.discovery; + +import java.time.Duration; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.JSON; +import io.kubernetes.client.openapi.models.CoreV1EndpointPort; +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointSubset; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsList; +import io.kubernetes.client.openapi.models.V1EndpointsListBuilder; +import io.kubernetes.client.openapi.models.V1ListMetaBuilder; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1ObjectMetaBuilder; +import io.kubernetes.client.openapi.models.V1ObjectReferenceBuilder; +import io.kubernetes.client.openapi.models.V1ServiceBuilder; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.openapi.models.V1ServiceListBuilder; +import io.kubernetes.client.openapi.models.V1ServicePortBuilder; +import io.kubernetes.client.openapi.models.V1ServiceSpecBuilder; +import io.kubernetes.client.util.ClientBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * @author Ryan Baxter + */ +class KubernetesClientConfigServerBootstrapperTests { + + private static WireMockServer wireMockServer; + + private ConfigurableApplicationContext context; + + @BeforeEach + public void before() throws JsonProcessingException { + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor(wireMockServer.port()); + + V1ServiceList SERVICE_LIST = new V1ServiceListBuilder() + .withMetadata(new V1ListMetaBuilder().withResourceVersion("1").build()) + .addToItems(new V1ServiceBuilder() + .withMetadata(new V1ObjectMetaBuilder().withName("spring-cloud-kubernetes-configserver") + .withNamespace("default").withResourceVersion("0").addToLabels("beta", "true") + .addToAnnotations("org.springframework.cloud", "true").withUid("0").build()) + .withSpec(new V1ServiceSpecBuilder().withClusterIP("localhost").withSessionAffinity("None") + .withType("ClusterIP") + .addToPorts(new V1ServicePortBuilder().withPort(wireMockServer.port()).withName("http") + .withProtocol("TCP").withNewTargetPort(wireMockServer.port()).build()) + .build()) + .build()) + .build(); + + V1EndpointsList ENDPOINTS_LIST = new V1EndpointsListBuilder() + .withMetadata(new V1ListMetaBuilder().withResourceVersion("0").build()) + .addToItems(new V1Endpoints() + .metadata(new V1ObjectMeta().name("spring-cloud-kubernetes-configserver").namespace("default")) + .addSubsetsItem( + new V1EndpointSubset() + .addPortsItem(new CoreV1EndpointPort().port(wireMockServer.port()).name("http")) + .addAddressesItem(new V1EndpointAddress().hostname("localhost").ip("localhost") + .targetRef(new V1ObjectReferenceBuilder().withUid("uid1").build())))) + .build(); + + Environment environment = new Environment("test", "default"); + Map properties = new HashMap<>(); + properties.put("hello", "world"); + org.springframework.cloud.config.environment.PropertySource p = new PropertySource("p1", properties); + environment.add(p); + ObjectMapper objectMapper = new ObjectMapper(); + stubFor(get("/application/default") + .willReturn(aResponse().withStatus(200).withBody(objectMapper.writeValueAsString(environment)) + .withHeader("content-type", "application/json"))); + stubFor(get("/api/v1/namespaces/default/endpoints?resourceVersion=0&watch=false") + .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(ENDPOINTS_LIST)) + .withHeader("content-type", "application/json"))); + stubFor(get("/api/v1/namespaces/default/services?resourceVersion=0&watch=false") + .willReturn(aResponse().withStatus(200).withBody(new JSON().serialize(SERVICE_LIST)) + .withHeader("content-type", "application/json"))); + stubFor(get(urlMatching("/api/v1/namespaces/default/services.*.watch=true")) + .willReturn(aResponse().withStatus(200))); + stubFor(get(urlMatching("/api/v1/namespaces/default/endpoints.*.watch=true")) + .willReturn(aResponse().withStatus(200))); + } + + @AfterEach + public void after() { + wireMockServer.stop(); + context.close(); + } + + @Test + void testBootstrapper() { + this.context = setup().run(); + verify(getRequestedFor(urlEqualTo("/application/default"))); + assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world"); + } + + SpringApplicationBuilder setup(String... env) { + SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class) + .properties(addDefaultEnv(env)); + ApiClient apiClient = new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()) + .setReadTimeout(Duration.ZERO).build(); + builder.addBootstrapRegistryInitializer(registry -> registry.register(ApiClient.class, (context) -> apiClient)); + builder.addBootstrapRegistryInitializer(new KubernetesClientConfigServerBootstrapper()); + return builder; + } + + private String[] addDefaultEnv(String[] env) { + Set set = new LinkedHashSet<>(); + if (env != null && env.length > 0) { + set.addAll(Arrays.asList(env)); + } + set.add("spring.cloud.config.discovery.enabled=true"); + set.add("spring.config.import=optional:configserver:"); + set.add("spring.cloud.config.discovery.service-id=spring-cloud-kubernetes-configserver"); + set.add("spring.cloud.kubernetes.client.namespace=default"); + return set.toArray(new String[0]); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + static class TestConfig { + + @Bean + public ApiClient apiClient() { + return new ClientBuilder().setBasePath("http://localhost:" + wireMockServer.port()).build(); + } + + } + +} diff --git a/spring-cloud-kubernetes-commons/pom.xml b/spring-cloud-kubernetes-commons/pom.xml index ef45de4e..08485ab8 100644 --- a/spring-cloud-kubernetes-commons/pom.xml +++ b/spring-cloud-kubernetes-commons/pom.xml @@ -75,6 +75,11 @@ spring-boot-starter-aop true + + org.springframework.cloud + spring-cloud-config-client + true + org.springframework.boot diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/KubernetesNamespaceProvider.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/KubernetesNamespaceProvider.java index ed5feca6..fa8d2ab4 100644 --- a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/KubernetesNamespaceProvider.java +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/KubernetesNamespaceProvider.java @@ -21,8 +21,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.logging.DeferredLog; import org.springframework.core.env.Environment; +import org.springframework.util.ObjectUtils; import static org.springframework.cloud.kubernetes.commons.KubernetesClientProperties.SERVICE_ACCOUNT_NAMESPACE_PATH; @@ -43,23 +47,49 @@ public class KubernetesNamespaceProvider { */ public static final String NAMESPACE_PATH_PROPERTY = "spring.cloud.kubernetes.client.serviceAccountNamespacePath"; + private BindHandler bindHandler; + private String serviceAccountNamespace; - private final Environment environment; + private Environment environment; + + private Binder binder; public KubernetesNamespaceProvider(Environment env) { this.environment = env; LOG.replayTo(KubernetesNamespaceProvider.class); } + public KubernetesNamespaceProvider(Binder binder, BindHandler bindHandler) { + this.binder = binder; + this.bindHandler = bindHandler; + } + public String getNamespace() { - String namespace = environment.getProperty(NAMESPACE_PROPERTY); + String namespace = null; + if (environment != null) { + namespace = environment.getProperty(NAMESPACE_PROPERTY); + } + if (ObjectUtils.isEmpty(namespace) && binder != null) { + namespace = binder.bind(NAMESPACE_PROPERTY, String.class).orElse(null); + } return namespace != null ? namespace : getServiceAccountNamespace(); } private String getServiceAccountNamespace() { - String serviceAccountNamespacePathString = environment.getProperty(NAMESPACE_PATH_PROPERTY, - SERVICE_ACCOUNT_NAMESPACE_PATH); + String serviceAccountNamespacePathString = null; + if (environment != null) { + serviceAccountNamespacePathString = environment.getProperty(NAMESPACE_PATH_PROPERTY, + SERVICE_ACCOUNT_NAMESPACE_PATH); + } + if (ObjectUtils.isEmpty(serviceAccountNamespacePathString) && binder != null) { + // When using the binder we cannot use camelcase properties, it considers them + // invalid + serviceAccountNamespacePathString = binder + .bind("spring.cloud.kubernetes.client.service-account-namespace-path", Bindable.of(String.class), + bindHandler) + .orElse(SERVICE_ACCOUNT_NAMESPACE_PATH); + } if (serviceAccountNamespace == null) { serviceAccountNamespace = getNamespaceFromServiceAccountFile(serviceAccountNamespacePathString); } diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerBootstrapper.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerBootstrapper.java new file mode 100644 index 00000000..3e829899 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerBootstrapper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.config; + +import org.springframework.boot.BootstrapRegistryInitializer; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.config.client.ConfigClientProperties; +import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; +import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.util.ClassUtils; + +/** + * @author Ryan Baxter + */ +public abstract class KubernetesConfigServerBootstrapper implements BootstrapRegistryInitializer { + + public static boolean hasConfigServerInstanceProvider() { + return !ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null); + } + + public static KubernetesDiscoveryProperties createKubernetesDiscoveryProperties(Binder binder, + BindHandler bindHandler) { + return binder.bind("spring.cloud.kubernetes.discovery", Bindable.of(KubernetesDiscoveryProperties.class), + bindHandler).orElseGet(() -> KubernetesDiscoveryProperties.DEFAULT); + } + + public static KubernetesClientProperties createKubernetesClientProperties(Binder binder, BindHandler bindHandler) { + return binder.bindOrCreate(KubernetesClientProperties.PREFIX, Bindable.of(KubernetesClientProperties.class)) + .withNamespace(new KubernetesNamespaceProvider(binder, bindHandler).getNamespace()); + } + + public static Boolean getDiscoveryEnabled(Binder binder, BindHandler bindHandler) { + return binder.bind(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED, Bindable.of(Boolean.class), bindHandler) + .orElse(false); + } + +} diff --git a/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerInstanceProvider.java b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerInstanceProvider.java new file mode 100644 index 00000000..c71e78a8 --- /dev/null +++ b/spring-cloud-kubernetes-commons/src/main/java/org/springframework/cloud/kubernetes/commons/config/KubernetesConfigServerInstanceProvider.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.commons.config; + +import java.util.List; + +import org.springframework.cloud.client.ServiceInstance; + +/** + * @author Ryan Baxter + */ +public interface KubernetesConfigServerInstanceProvider { + + List getInstances(String serviceId); + +} diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsAutoConfigurationTests.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsAutoConfigurationTests.java index 4c6d9b62..13477539 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsAutoConfigurationTests.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/KubernetesCommonsAutoConfigurationTests.java @@ -28,7 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = KubernetesCommonsAutoConfigurationTests.App.class, properties = { "spring.main.cloud-platform=KUBERNETES", "spring.cloud.kubernetes.client.password=mypassword", - "spring.cloud.kubernetes.client.proxy-password=myproxypassword" }) + "spring.cloud.kubernetes.client.proxy-password=myproxypassword", "spring.cloud.config.enabled=false" }) class KubernetesCommonsAutoConfigurationTests { @Autowired diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigAndSecretsFailFastEnabledWithDefaultRetryConfiguration.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigAndSecretsFailFastEnabledWithDefaultRetryConfiguration.java index 9e12e22d..a9d8755d 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigAndSecretsFailFastEnabledWithDefaultRetryConfiguration.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigAndSecretsFailFastEnabledWithDefaultRetryConfiguration.java @@ -36,7 +36,8 @@ import static org.assertj.core.api.Assertions.assertThat; */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class, properties = { "spring.cloud.kubernetes.config.fail-fast=true", - "spring.cloud.kubernetes.secrets.fail-fast=true", "spring.main.cloud-platform=KUBERNETES" }) + "spring.cloud.kubernetes.secrets.fail-fast=true", "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.config.enabled=false" }) class ConfigAndSecretsFailFastEnabledWithDefaultRetryConfiguration { @Autowired diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabled.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabled.java index 8b564c56..9efbfcd8 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabled.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabled.java @@ -34,7 +34,8 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Isik Erhan */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class, - properties = { "spring.cloud.kubernetes.config.fail-fast=true", "spring.main.cloud-platform=KUBERNETES" }) + properties = { "spring.cloud.kubernetes.config.fail-fast=true", "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.config.enabled=false" }) class ConfigFailFastEnabled { @Autowired diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledButRetryDisabled.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledButRetryDisabled.java index 84234d29..4a798e99 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledButRetryDisabled.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledButRetryDisabled.java @@ -29,8 +29,9 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Isik Erhan */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class, properties = { - "spring.cloud.kubernetes.config.fail-fast=true", "spring.cloud.kubernetes.config.retry.enabled=false" }) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class, + properties = { "spring.cloud.kubernetes.config.fail-fast=true", + "spring.cloud.kubernetes.config.retry.enabled=false", "spring.cloud.config.enabled=false" }) class ConfigFailFastEnabledButRetryDisabled { @Autowired diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledWithCustomRetryConfiguration.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledWithCustomRetryConfiguration.java index 52e3c355..9dadffa6 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledWithCustomRetryConfiguration.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/ConfigFailFastEnabledWithCustomRetryConfiguration.java @@ -29,12 +29,11 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Isik Erhan */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class, - properties = { "spring.cloud.kubernetes.config.fail-fast=true", - "spring.cloud.kubernetes.config.retry.max-attempts=3", - "spring.cloud.kubernetes.config.retry.initial-interval=1500", - "spring.cloud.kubernetes.config.retry.max-interval=3000", - "spring.cloud.kubernetes.config.retry.multiplier=1.5", "spring.main.cloud-platform=KUBERNETES" }) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class, properties = { + "spring.cloud.kubernetes.config.fail-fast=true", "spring.cloud.kubernetes.config.retry.max-attempts=3", + "spring.cloud.kubernetes.config.retry.initial-interval=1500", + "spring.cloud.kubernetes.config.retry.max-interval=3000", "spring.cloud.kubernetes.config.retry.multiplier=1.5", + "spring.main.cloud-platform=KUBERNETES", "spring.cloud.config.enabled=false" }) class ConfigFailFastEnabledWithCustomRetryConfiguration { @Autowired diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/FailFastDisabled.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/FailFastDisabled.java index 63ab8a0c..9cbd6980 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/FailFastDisabled.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/FailFastDisabled.java @@ -29,7 +29,8 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Isik Erhan */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class, + properties = { "spring.cloud.config.enabled=false" }) class FailFastDisabled { @Autowired diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabled.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabled.java index a640da72..8263961c 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabled.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabled.java @@ -34,7 +34,8 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Isik Erhan */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class, - properties = { "spring.cloud.kubernetes.secrets.fail-fast=true", "spring.main.cloud-platform=KUBERNETES" }) + properties = { "spring.cloud.kubernetes.secrets.fail-fast=true", "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.config.enabled=false" }) class SecretsFailFastEnabled { @Autowired diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledButRetryDisabled.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledButRetryDisabled.java index 6925dd8f..8b32ddc1 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledButRetryDisabled.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledButRetryDisabled.java @@ -29,8 +29,9 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Isik Erhan */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class, properties = { - "spring.cloud.kubernetes.secrets.fail-fast=true", "spring.cloud.kubernetes.secrets.retry.enabled=false" }) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = App.class, + properties = { "spring.cloud.kubernetes.secrets.fail-fast=true", + "spring.cloud.kubernetes.secrets.retry.enabled=false", "spring.cloud.config.enabled=false" }) class SecretsFailFastEnabledButRetryDisabled { @Autowired diff --git a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledWithCustomRetryConfiguration.java b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledWithCustomRetryConfiguration.java index b4d54048..51e0b204 100644 --- a/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledWithCustomRetryConfiguration.java +++ b/spring-cloud-kubernetes-commons/src/test/java/org/springframework/cloud/kubernetes/commons/config/bootstrap/SecretsFailFastEnabledWithCustomRetryConfiguration.java @@ -34,7 +34,8 @@ import static org.assertj.core.api.Assertions.assertThat; "spring.cloud.kubernetes.secrets.retry.max-attempts=3", "spring.cloud.kubernetes.secrets.retry.initial-interval=1500", "spring.cloud.kubernetes.secrets.retry.max-interval=3000", - "spring.cloud.kubernetes.secrets.retry.multiplier=1.5", "spring.main.cloud-platform=KUBERNETES" }) + "spring.cloud.kubernetes.secrets.retry.multiplier=1.5", "spring.main.cloud-platform=KUBERNETES", + "spring.cloud.config.enabled=false" }) class SecretsFailFastEnabledWithCustomRetryConfiguration { @Autowired diff --git a/spring-cloud-kubernetes-discovery/pom.xml b/spring-cloud-kubernetes-discovery/pom.xml index 668a02d8..2325187a 100644 --- a/spring-cloud-kubernetes-discovery/pom.xml +++ b/spring-cloud-kubernetes-discovery/pom.xml @@ -21,6 +21,15 @@ org.springframework.cloud spring-cloud-commons + + org.springframework.cloud + spring-cloud-kubernetes-commons + + + org.springframework.cloud + spring-cloud-config-client + true + org.springframework.boot spring-boot-actuator diff --git a/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapper.java b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapper.java new file mode 100644 index 00000000..a68d89b8 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapper.java @@ -0,0 +1,106 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.Log; + +import org.springframework.boot.BootstrapContext; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.config.client.ConfigServerInstanceProvider; +import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; +import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper; +import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.util.ClassUtils; + +/** + * @author Ryan Baxter + */ +class ConfigServerBootstrapper extends KubernetesConfigServerBootstrapper { + + @Override + public void initialize(BootstrapRegistry registry) { + if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) { + return; + } + // We need to pass a lambda here rather than create a new instance of + // ConfigServerInstanceProvider.Function + // or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on + // the classpath + registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create); + } + + final static class KubernetesFunction implements ConfigServerInstanceProvider.Function { + + private final BootstrapContext context; + + private KubernetesFunction(BootstrapContext context) { + this.context = context; + } + + static KubernetesFunction create(BootstrapContext context) { + return new KubernetesFunction(context); + } + + @Override + public List apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) { + if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) { + // If we don't have the Binder or BinderHandler from the + // ConfigDataLocationResolverContext + // we won't be able to create the necessary configuration + // properties to configure the + // Kubernetes DiscoveryClient + return Collections.emptyList(); + } + KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder, + bindHandler); + KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler); + return getInstanceProvider(discoveryProperties, clientProperties, context, binder, bindHandler, log) + .getInstances(serviceId); + } + + private KubernetesConfigServerInstanceProvider getInstanceProvider( + KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties, + BootstrapContext context, Binder binder, BindHandler bindHandler, Log log) { + KubernetesDiscoveryClientProperties kubernetesDiscoveryClientProperties = binder + .bind("spring.cloud.kubernetes.discovery", Bindable.of(KubernetesDiscoveryClientProperties.class), + bindHandler) + .orElseGet(KubernetesDiscoveryClientProperties::new); + KubernetesDiscoveryClientAutoConfiguration.Servlet autoConfiguration = new KubernetesDiscoveryClientAutoConfiguration.Servlet(); + DiscoveryClient discoveryClient = autoConfiguration + .kubernetesDiscoveryClient(autoConfiguration.restTemplate(), kubernetesDiscoveryClientProperties); + return discoveryClient::getInstances; + } + + // This method should never be called, but is there for backward + // compatibility purposes + @Override + public List apply(String serviceId) { + return apply(serviceId, null, null, null); + } + + } + +} diff --git a/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..4fbb2223 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.BootstrapRegistryInitializer=\ +org.springframework.cloud.kubernetes.discovery.ConfigServerBootstrapper diff --git a/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapperTests.java b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapperTests.java new file mode 100644 index 00000000..bf1d47e0 --- /dev/null +++ b/spring-cloud-kubernetes-discovery/src/test/java/org/springframework/cloud/kubernetes/discovery/ConfigServerBootstrapperTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.discovery; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.context.ConfigurableApplicationContext; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ryan Baxter + */ +class ConfigServerBootstrapperTests { + + private static WireMockServer wireMockServer; + + protected ConfigurableApplicationContext context; + + @AfterEach + void close() { + wireMockServer.stop(); + if (this.context != null) { + this.context.close(); + } + } + + @BeforeEach + void beforeAll() throws JsonProcessingException { + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor(wireMockServer.port()); + String APPS_NAME = "[{\"instanceId\":\"uid2\",\"serviceId\":\"spring-cloud-kubernetes-configserver\",\"host\":\"localhost\",\"port\":" + + wireMockServer.port() + ",\"uri\":\"" + wireMockServer.baseUrl() + + "\",\"secure\":false,\"metadata\":{\"spring\":\"true\",\"http\":\"8080\",\"k8s\":\"true\"},\"namespace\":\"namespace1\",\"cluster\":null,\"scheme\":\"http\"}]"; + stubFor(get("/apps/spring-cloud-kubernetes-configserver").willReturn( + aResponse().withStatus(200).withBody(APPS_NAME).withHeader("content-type", "application/json"))); + Environment environment = new Environment("test", "default"); + Map properties = new HashMap<>(); + properties.put("hello", "world"); + PropertySource p = new PropertySource("p1", properties); + environment.add(p); + ObjectMapper objectMapper = new ObjectMapper(); + stubFor(get("/application/default") + .willReturn(aResponse().withStatus(200).withBody(objectMapper.writeValueAsString(environment)) + .withHeader("content-type", "application/json"))); + } + + @Test + void testBootstrapper() { + this.context = setup().run(); + verify(1, getRequestedFor(urlEqualTo("/apps/spring-cloud-kubernetes-configserver"))); + assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world"); + } + + SpringApplicationBuilder setup(String... env) { + SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class) + .properties(addDefaultEnv(env)); + builder.addBootstrapRegistryInitializer(new ConfigServerBootstrapper()); + return builder; + } + + private String[] addDefaultEnv(String[] env) { + Set set = new LinkedHashSet<>(); + if (env != null && env.length > 0) { + set.addAll(Arrays.asList(env)); + } + set.add("spring.cloud.config.discovery.enabled=true"); + set.add("spring.config.import=optional:configserver:"); + set.add("spring.cloud.config.discovery.service-id=spring-cloud-kubernetes-configserver"); + set.add("spring.cloud.kubernetes.discovery.discoveryServerUrl=" + wireMockServer.baseUrl()); + return set.toArray(new String[0]); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + static class TestConfig { + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-discovery/pom.xml b/spring-cloud-kubernetes-fabric8-discovery/pom.xml index 79ae9f35..b509b182 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/pom.xml +++ b/spring-cloud-kubernetes-fabric8-discovery/pom.xml @@ -42,6 +42,11 @@ org.springframework.cloud spring-cloud-context + + org.springframework.cloud + spring-cloud-config-client + true + @@ -102,13 +107,13 @@ test - org.springframework.cloud - spring-cloud-config-client + io.projectreactor + reactor-test test - io.projectreactor - reactor-test + com.github.tomakehurst + wiremock-jre8-standalone test diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapper.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapper.java new file mode 100644 index 00000000..17231f86 --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapper.java @@ -0,0 +1,112 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.discovery; + +import java.util.Collections; +import java.util.List; + +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.apache.commons.logging.Log; + +import org.springframework.boot.BootstrapContext; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.config.client.ConfigServerInstanceProvider; +import org.springframework.cloud.kubernetes.commons.KubernetesClientProperties; +import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerBootstrapper; +import org.springframework.cloud.kubernetes.commons.config.KubernetesConfigServerInstanceProvider; +import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; +import org.springframework.cloud.kubernetes.fabric8.Fabric8AutoConfiguration; +import org.springframework.util.ClassUtils; + +/** + * @author Ryan Baxter + */ +class Fabric8ConfigServerBootstrapper extends KubernetesConfigServerBootstrapper { + + @Override + public void initialize(BootstrapRegistry registry) { + if (!ClassUtils.isPresent("org.springframework.cloud.config.client.ConfigServerInstanceProvider", null)) { + return; + } + // We need to pass a lambda here rather than create a new instance of + // ConfigServerInstanceProvider.Function + // or else we will get ClassNotFoundExceptions if Spring Cloud Config is not on + // the classpath + registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, KubernetesFunction::create); + } + + final static class KubernetesFunction implements ConfigServerInstanceProvider.Function { + + private final BootstrapContext context; + + private KubernetesFunction(BootstrapContext context) { + this.context = context; + } + + static KubernetesFunction create(BootstrapContext context) { + return new KubernetesFunction(context); + } + + @Override + public List apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) { + if (binder == null || bindHandler == null || !getDiscoveryEnabled(binder, bindHandler)) { + // If we don't have the Binder or BinderHandler from the + // ConfigDataLocationResolverContext + // we won't be able to create the necessary configuration + // properties to configure the + // Kubernetes DiscoveryClient + return Collections.emptyList(); + } + KubernetesDiscoveryProperties discoveryProperties = createKubernetesDiscoveryProperties(binder, + bindHandler); + KubernetesClientProperties clientProperties = createKubernetesClientProperties(binder, bindHandler); + return getInstanceProvider(discoveryProperties, clientProperties, context, binder, bindHandler).getInstances(serviceId); + } + + private KubernetesConfigServerInstanceProvider getInstanceProvider( + KubernetesDiscoveryProperties discoveryProperties, KubernetesClientProperties clientProperties, + BootstrapContext context, Binder binder, BindHandler bindHandler) { + if (context.isRegistered(KubernetesDiscoveryClient.class)) { + KubernetesDiscoveryClient client = context.get(KubernetesDiscoveryClient.class); + return client::getInstances; + } + else { + Fabric8AutoConfiguration fabric8AutoConfiguration = new Fabric8AutoConfiguration(); + Config config = fabric8AutoConfiguration.kubernetesClientConfig(clientProperties); + KubernetesClient kubernetesClient = fabric8AutoConfiguration.kubernetesClient(config); + KubernetesDiscoveryClient discoveryClient = new KubernetesDiscoveryClient(kubernetesClient, + discoveryProperties, + KubernetesClientServicesFunctionProvider.servicesFunction(discoveryProperties, binder, bindHandler), null, + new ServicePortSecureResolver(discoveryProperties)); + return discoveryClient::getInstances; + } + } + + // This method should never be called, but is there for backward + // compatibility purposes + @Override + public List apply(String serviceId) { + return apply(serviceId, null, null, null); + } + + } + +} diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java index 583db5e9..820d3a3f 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/java/org/springframework/cloud/kubernetes/fabric8/discovery/KubernetesClientServicesFunctionProvider.java @@ -16,6 +16,8 @@ package org.springframework.cloud.kubernetes.fabric8.discovery; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.cloud.kubernetes.commons.KubernetesNamespaceProvider; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; import org.springframework.cloud.kubernetes.fabric8.Fabric8Utils; @@ -44,4 +46,19 @@ public final class KubernetesClientServicesFunctionProvider { } + public static KubernetesClientServicesFunction servicesFunction(KubernetesDiscoveryProperties properties, + Binder binder, BindHandler bindHandler) { + + if (properties.allNamespaces()) { + return (client) -> client.services().inAnyNamespace().withLabels(properties.serviceLabels()); + } + + return client -> { + String namespace = Fabric8Utils.getApplicationNamespace(client, null, "discovery-service", + new KubernetesNamespaceProvider(binder, bindHandler)); + return client.services().inNamespace(namespace).withLabels(properties.serviceLabels()); + }; + + } + } diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/main/resources/META-INF/spring.factories b/spring-cloud-kubernetes-fabric8-discovery/src/main/resources/META-INF/spring.factories index 7262a091..d1cfd557 100644 --- a/spring-cloud-kubernetes-fabric8-discovery/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-kubernetes-fabric8-discovery/src/main/resources/META-INF/spring.factories @@ -1,2 +1,4 @@ org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClientConfigClientBootstrapConfiguration +org.springframework.boot.BootstrapRegistryInitializer=\ +org.springframework.cloud.kubernetes.fabric8.discovery.Fabric8ConfigServerBootstrapper diff --git a/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapperTests.java b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapperTests.java new file mode 100644 index 00000000..b1420b2f --- /dev/null +++ b/spring-cloud-kubernetes-fabric8-discovery/src/test/java/org/springframework/cloud/kubernetes/fabric8/discovery/Fabric8ConfigServerBootstrapperTests.java @@ -0,0 +1,145 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.kubernetes.fabric8.discovery; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import io.fabric8.kubernetes.api.model.Endpoints; +import io.fabric8.kubernetes.api.model.EndpointsBuilder; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.ServiceSpecBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.context.ConfigurableApplicationContext; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * @author Ryan Baxter + */ +@EnableKubernetesMockClient(crud = true, https = false) +class Fabric8ConfigServerBootstrapperTests { + + private static WireMockServer wireMockServer; + + private KubernetesClient mockClient; + + private ConfigurableApplicationContext context; + + @BeforeEach + public void before() throws JsonProcessingException { + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor(wireMockServer.port()); + + // Configure the kubernetes master url to point to the mock server + System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, mockClient.getConfiguration().getMasterUrl()); + System.setProperty(Config.KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, "true"); + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_HTTP2_DISABLE, "true"); + + Endpoints endPoint = new EndpointsBuilder().withNewMetadata().withName("spring-cloud-kubernetes-configserver") + .withNamespace("test").withLabels(new HashMap<>()).endMetadata().addNewSubset().addNewAddress() + .withHostname("localhost").withIp("localhost").withNewTargetRef().withUid("10").endTargetRef() + .endAddress().addNewPort("http", "http_tcp", wireMockServer.port(), "TCP").endSubset().build(); + + mockClient.endpoints().inNamespace("test").create(endPoint); + + Service service = new ServiceBuilder().withNewMetadata().withName("spring-cloud-kubernetes-configserver") + .withNamespace("test").withLabels(new HashMap<>()).endMetadata() + .withSpec(new ServiceSpecBuilder().withType("NodePort").build()).build(); + + mockClient.services().inNamespace("test").create(service); + + Environment environment = new Environment("test", "default"); + Map properties = new HashMap<>(); + properties.put("hello", "world"); + org.springframework.cloud.config.environment.PropertySource p = new PropertySource("p1", properties); + environment.add(p); + ObjectMapper objectMapper = new ObjectMapper(); + stubFor(get("/application/default") + .willReturn(aResponse().withStatus(200).withBody(objectMapper.writeValueAsString(environment)) + .withHeader("content-type", "application/json"))); + } + + @AfterEach + public void after() { + wireMockServer.stop(); + mockClient.close(); + context.close(); + } + + @Test + void testBootstrapper() { + this.context = setup().run(); + verify(getRequestedFor(urlEqualTo("/application/default"))); + assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world"); + } + + SpringApplicationBuilder setup(String... env) { + SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class) + .properties(addDefaultEnv(env)); + builder.addBootstrapRegistryInitializer(new Fabric8ConfigServerBootstrapper()); + return builder; + } + + private String[] addDefaultEnv(String[] env) { + Set set = new LinkedHashSet<>(); + if (env != null && env.length > 0) { + set.addAll(Arrays.asList(env)); + } + set.add("spring.cloud.config.discovery.enabled=true"); + set.add("spring.config.import=optional:configserver:"); + set.add("spring.cloud.config.discovery.service-id=spring-cloud-kubernetes-configserver"); + set.add("spring.cloud.kubernetes.client.namespace=test"); + return set.toArray(new String[0]); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + static class TestConfig { + + } + +}