diff --git a/pom.xml b/pom.xml index fbd80a720..5096a6f78 100644 --- a/pom.xml +++ b/pom.xml @@ -27,6 +27,7 @@ 3.1.8-SNAPSHOT 3.2.9-SNAPSHOT 1.17.6 + 5.15.0 @@ -156,11 +157,21 @@ consul ${testcontainers.version} + + org.testcontainers + mockserver + ${testcontainers.version} + org.testcontainers junit-jupiter ${testcontainers.version} + + org.mock-server + mockserver-client-java + ${mockserverclient.version} + diff --git a/spring-cloud-consul-discovery/pom.xml b/spring-cloud-consul-discovery/pom.xml index 41e1b7ad3..839a6a55c 100644 --- a/spring-cloud-consul-discovery/pom.xml +++ b/spring-cloud-consul-discovery/pom.xml @@ -134,6 +134,16 @@ consul test + + org.testcontainers + mockserver + test + + + org.mock-server + mockserver-client-java + test + org.testcontainers junit-jupiter diff --git a/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/configclient/ConsulConfigServerBootstrapper.java b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/configclient/ConsulConfigServerBootstrapper.java index 30a35d5d8..22fcd4fa2 100644 --- a/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/configclient/ConsulConfigServerBootstrapper.java +++ b/spring-cloud-consul-discovery/src/main/java/org/springframework/cloud/consul/discovery/configclient/ConsulConfigServerBootstrapper.java @@ -17,14 +17,18 @@ package org.springframework.cloud.consul.discovery.configclient; import java.util.Collections; +import java.util.List; import com.ecwid.consul.v1.ConsulClient; +import org.apache.commons.logging.Log; +import org.springframework.boot.BootstrapContext; import org.springframework.boot.BootstrapRegistry; 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.client.ServiceInstance; import org.springframework.cloud.commons.util.InetUtils; import org.springframework.cloud.commons.util.InetUtilsProperties; import org.springframework.cloud.config.client.ConfigClientProperties; @@ -84,24 +88,50 @@ public void initialize(BootstrapRegistry registry) { discoveryClient); } }); - registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, context -> { - if (!isDiscoveryEnabled(context.get(Binder.class))) { - return (id) -> Collections.emptyList(); - } - ConsulDiscoveryClient discoveryClient = context.get(ConsulDiscoveryClient.class); - return discoveryClient::getInstances; - }); - + registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, ConsulFunction::new); } private BindHandler getBindHandler(org.springframework.boot.BootstrapContext context) { return context.getOrElse(BindHandler.class, null); } - private boolean isDiscoveryEnabled(Binder binder) { + private static boolean isDiscoveryEnabled(Binder binder) { return binder.bind(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED, Boolean.class).orElse(false) && binder.bind(ConditionalOnConsulDiscoveryEnabled.PROPERTY, Boolean.class).orElse(true) && binder.bind("spring.cloud.discovery.enabled", Boolean.class).orElse(true); } + static final class ConsulFunction implements ConfigServerInstanceProvider.Function { + + private final BootstrapContext context; + + private ConsulFunction(BootstrapContext context) { + this.context = context; + } + + @Override + public List apply(String serviceId) { + return apply(serviceId, null, null, null); + } + + @Override + public List apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) { + if (binder == null || !isDiscoveryEnabled(binder)) { + return Collections.emptyList(); + } + + ConsulProperties consulProperties = binder + .bind(ConsulProperties.PREFIX, Bindable.of(ConsulProperties.class), bindHandler) + .orElseGet(ConsulProperties::new); + ConsulClient consulClient = ConsulAutoConfiguration.createConsulClient(consulProperties); + ConsulDiscoveryProperties properties = binder + .bind(ConsulDiscoveryProperties.PREFIX, Bindable.of(ConsulDiscoveryProperties.class), bindHandler) + .orElseGet(() -> new ConsulDiscoveryProperties(new InetUtils(new InetUtilsProperties()))); + ConsulDiscoveryClient discoveryClient = new ConsulDiscoveryClient(consulClient, properties); + + return discoveryClient.getInstances(serviceId); + } + + } + } diff --git a/spring-cloud-consul-discovery/src/test/java/org/springframework/cloud/consul/discovery/configclient/ConsulConfigServerBootstrapperIT.java b/spring-cloud-consul-discovery/src/test/java/org/springframework/cloud/consul/discovery/configclient/ConsulConfigServerBootstrapperIT.java new file mode 100644 index 000000000..ff0a38144 --- /dev/null +++ b/spring-cloud-consul-discovery/src/test/java/org/springframework/cloud/consul/discovery/configclient/ConsulConfigServerBootstrapperIT.java @@ -0,0 +1,135 @@ +/* + * 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.consul.discovery.configclient; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import com.ecwid.consul.v1.ConsulClient; +import com.ecwid.consul.v1.agent.model.NewService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockserver.client.MockServerClient; +import org.testcontainers.consul.ConsulContainer; +import org.testcontainers.containers.MockServerContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +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.cloud.consul.ConsulAutoConfiguration; +import org.springframework.cloud.consul.ConsulProperties; +import org.springframework.cloud.consul.test.ConsulTestcontainers; +import org.springframework.context.ConfigurableApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; + +/** + * @author Ryan Baxter + */ + +@Testcontainers +public class ConsulConfigServerBootstrapperIT { + + public static final DockerImageName MOCKSERVER_IMAGE = DockerImageName.parse("mockserver/mockserver") + .withTag("mockserver-" + MockServerClient.class.getPackage().getImplementationVersion()); + + @Container + static ConsulContainer consul = ConsulTestcontainers.createConsulContainer("1.10"); + + @Container + static MockServerContainer mockServer = new MockServerContainer(MOCKSERVER_IMAGE); + + private ConfigurableApplicationContext context; + + @BeforeEach + void before() { + ConsulProperties consulProperties = new ConsulProperties(); + consulProperties.setHost(consul.getHost()); + consulProperties.setPort(consul.getMappedPort(ConsulTestcontainers.DEFAULT_PORT)); + ConsulClient client = ConsulAutoConfiguration.createConsulClient(consulProperties); + NewService newService = new NewService(); + newService.setId("consul-configserver"); + newService.setName("consul-configserver"); + newService.setAddress(mockServer.getHost()); + newService.setPort(mockServer.getServerPort()); + client.agentServiceRegister(newService); + + } + + @AfterEach + void after() { + this.context.close(); + } + + @Test + public void contextLoads() throws JsonProcessingException { + 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(); + try (MockServerClient mockServerClient = new MockServerClient(mockServer.getHost(), + mockServer.getMappedPort(MockServerContainer.PORT))) { + mockServerClient.when(request().withPath("/application/default")) + .respond(response().withBody(objectMapper.writeValueAsString(environment)) + .withHeader("content-type", "application/json")); + this.context = setup().run(); + assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world"); + } + + } + + SpringApplicationBuilder setup(String... env) { + SpringApplicationBuilder builder = new SpringApplicationBuilder(TestConfig.class) + .properties(addDefaultEnv(env)); + 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.config.import=classpath:bootstrapper.yaml"); + set.add("spring.cloud.config.enabled=true"); + set.add("spring.cloud.service-registry.auto-registration.enabled=false"); + set.add(ConsulProperties.PREFIX + ".host=" + consul.getHost()); + set.add(ConsulProperties.PREFIX + ".port=" + consul.getMappedPort(ConsulTestcontainers.DEFAULT_PORT)); + return set.toArray(new String[0]); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + static class TestConfig { + + } + +} diff --git a/spring-cloud-consul-discovery/src/test/java/org/springframework/cloud/consul/discovery/configclient/ConsulConfigServerBootstrapperTests.java b/spring-cloud-consul-discovery/src/test/java/org/springframework/cloud/consul/discovery/configclient/ConsulConfigServerBootstrapperTests.java index dc1d61e91..1645de90b 100644 --- a/spring-cloud-consul-discovery/src/test/java/org/springframework/cloud/consul/discovery/configclient/ConsulConfigServerBootstrapperTests.java +++ b/spring-cloud-consul-discovery/src/test/java/org/springframework/cloud/consul/discovery/configclient/ConsulConfigServerBootstrapperTests.java @@ -20,6 +20,7 @@ import java.util.concurrent.atomic.AtomicReference; import com.ecwid.consul.transport.TransportException; +import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; import org.springframework.boot.BootstrapRegistry; @@ -29,14 +30,27 @@ import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.properties.bind.BindContext; import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.BindResult; import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.cloud.commons.util.InetUtilsProperties; +import org.springframework.cloud.config.client.ConfigClientProperties; import org.springframework.cloud.config.client.ConfigServerInstanceProvider; +import org.springframework.cloud.consul.ConsulProperties; +import org.springframework.cloud.consul.discovery.ConditionalOnConsulDiscoveryEnabled; import org.springframework.cloud.consul.discovery.ConsulDiscoveryClient; +import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties; import org.springframework.context.ConfigurableApplicationContext; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class ConsulConfigServerBootstrapperTests { @@ -47,7 +61,14 @@ public void notEnabledDoesNotAddInstanceProviderFn() { .addBootstrapRegistryInitializer(registry -> registry.addCloseListener(event -> { ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext() .get(ConfigServerInstanceProvider.Function.class); - assertThat(providerFn.apply("id")) + BindResult falseBindResult = mock(BindResult.class); + when(falseBindResult.orElse(anyBoolean())).thenReturn(false); + Binder binder = mock(Binder.class); + when(binder.bind(eq(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED), eq(Boolean.class))) + .thenReturn(falseBindResult); + BindHandler bindHandler = mock(BindHandler.class); + Log log = mock(Log.class); + assertThat(providerFn.apply("id", binder, bindHandler, log)) .as("ConfigServerInstanceProvider.Function should return empty list") .isEqualTo(Collections.EMPTY_LIST); })).run().close(); @@ -56,12 +77,22 @@ public void notEnabledDoesNotAddInstanceProviderFn() { @Test public void consulDiscoveryClientNotEnabledProvidesEmptyList() { new SpringApplicationBuilder(TestConfig.class) - .properties("--server.port=0", "spring.cloud.service-registry.auto-registration.enabled=false", - "spring.cloud.config.discovery.enabled=true", "spring.cloud.consul.discovery.enabled=false") + .properties("--server.port=0", "spring.cloud.service-registry.auto-registration.enabled=false") .addBootstrapRegistryInitializer(registry -> registry.addCloseListener(event -> { ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext() .get(ConfigServerInstanceProvider.Function.class); - assertThat(providerFn.apply("id")) + BindResult falseBindResult = mock(BindResult.class); + when(falseBindResult.orElse(anyBoolean())).thenReturn(false); + BindResult trueBindResult = mock(BindResult.class); + when(trueBindResult.orElse(anyBoolean())).thenReturn(true); + Binder binder = mock(Binder.class); + when(binder.bind(eq(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED), eq(Boolean.class))) + .thenReturn(trueBindResult); + when(binder.bind(eq(ConditionalOnConsulDiscoveryEnabled.PROPERTY), eq(Boolean.class))) + .thenReturn(falseBindResult); + BindHandler bindHandler = mock(BindHandler.class); + Log log = mock(Log.class); + assertThat(providerFn.apply("id", binder, bindHandler, log)) .as("ConfigServerInstanceProvider.Function should return empty list") .isEqualTo(Collections.EMPTY_LIST); })).run().close(); @@ -70,12 +101,24 @@ public void consulDiscoveryClientNotEnabledProvidesEmptyList() { @Test public void springCloudDiscoveryClientNotEnabledProvidesEmptyList() { new SpringApplicationBuilder(TestConfig.class) - .properties("--server.port=0", "spring.cloud.service-registry.auto-registration.enabled=false", - "spring.cloud.config.discovery.enabled=true", "spring.cloud.discovery.enabled=false") + .properties("--server.port=0", "spring.cloud.service-registry.auto-registration.enabled=false") .addBootstrapRegistryInitializer(registry -> registry.addCloseListener(event -> { ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext() .get(ConfigServerInstanceProvider.Function.class); - assertThat(providerFn.apply("id")) + BindResult falseBindResult = mock(BindResult.class); + when(falseBindResult.orElse(anyBoolean())).thenReturn(false); + BindResult trueBindResult = mock(BindResult.class); + when(trueBindResult.orElse(anyBoolean())).thenReturn(true); + Binder binder = mock(Binder.class); + when(binder.bind(eq(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED), eq(Boolean.class))) + .thenReturn(trueBindResult); + when(binder.bind(eq(ConditionalOnConsulDiscoveryEnabled.PROPERTY), eq(Boolean.class))) + .thenReturn(trueBindResult); + when(binder.bind(eq("spring.cloud.discovery.enabled"), eq(Boolean.class))) + .thenReturn(falseBindResult); + BindHandler bindHandler = mock(BindHandler.class); + Log log = mock(Log.class); + assertThat(providerFn.apply("id", binder, bindHandler, log)) .as("ConfigServerInstanceProvider.Function should return empty list") .isEqualTo(Collections.EMPTY_LIST); })).run().close(); @@ -88,14 +131,45 @@ public void enabledAddsInstanceProviderFn() { ConfigurableApplicationContext context = new SpringApplicationBuilder(TestConfig.class) .properties("--server.port=0", "spring.cloud.config.discovery.enabled=true", "spring.cloud.consul.discovery.hostname=myhost", - "spring.cloud.service-registry.auto-registration.enabled=false", - "spring.cloud.consul.host=localhost") + "spring.cloud.service-registry.auto-registration.enabled=false") .addBootstrapRegistryInitializer(bindHandlerBootstrapper) .addBootstrapRegistryInitializer(registry -> registry.addCloseListener(event -> { bootstrapDiscoveryClient.set(event.getBootstrapContext().get(ConsulDiscoveryClient.class)); ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext() .get(ConfigServerInstanceProvider.Function.class); - assertThatThrownBy(() -> providerFn.apply("id")).isInstanceOf(TransportException.class) + + BindHandler bindHandler = mock(BindHandler.class); + Binder binder = mock(Binder.class); + Log log = mock(Log.class); + + BindResult falseBindResult = mock(BindResult.class); + when(falseBindResult.orElse(anyBoolean())).thenReturn(false); + BindResult trueBindResult = mock(BindResult.class); + when(trueBindResult.orElse(anyBoolean())).thenReturn(true); + + when(binder.bind(eq(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED), eq(Boolean.class))) + .thenReturn(trueBindResult); + when(binder.bind(eq(ConditionalOnConsulDiscoveryEnabled.PROPERTY), eq(Boolean.class))) + .thenReturn(trueBindResult); + when(binder.bind(eq("spring.cloud.discovery.enabled"), eq(Boolean.class))) + .thenReturn(trueBindResult); + + BindResult consulPropertiesResult = mock(BindResult.class); + when(consulPropertiesResult.orElseGet(any())).thenReturn(new ConsulProperties()); + when(binder.bind(ConsulProperties.PREFIX, Bindable.of(ConsulProperties.class), bindHandler)) + .thenReturn(consulPropertiesResult); + + BindResult consulDiscoveryPropertiesResult = mock(BindResult.class); + ConsulDiscoveryProperties discoveryProperties = new ConsulDiscoveryProperties( + new InetUtils(new InetUtilsProperties())); + discoveryProperties.setHostname("myhost"); + when(consulDiscoveryPropertiesResult.orElseGet(any())).thenReturn(discoveryProperties); + + when(binder.bind(ConsulDiscoveryProperties.PREFIX, Bindable.of(ConsulDiscoveryProperties.class), + bindHandler)).thenReturn(consulDiscoveryPropertiesResult); + + assertThatThrownBy(() -> providerFn.apply("id", binder, bindHandler, log)) + .isInstanceOf(TransportException.class) .hasMessageContaining( "org.apache.http.conn.HttpHostConnectException: Connect to localhost:8500") .as("Should have tried to reach out to Consul to get config server instance").isNotNull(); diff --git a/spring-cloud-consul-discovery/src/test/resources/bootstrapper.yaml b/spring-cloud-consul-discovery/src/test/resources/bootstrapper.yaml new file mode 100644 index 000000000..13b4e728e --- /dev/null +++ b/spring-cloud-consul-discovery/src/test/resources/bootstrapper.yaml @@ -0,0 +1,8 @@ +spring: + config: + import: "optional:configserver:" + cloud: + config: + discovery: + service-id: consul-configserver + enabled: true diff --git a/spring-cloud-consul-discovery/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/spring-cloud-consul-discovery/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..ca6ee9cea --- /dev/null +++ b/spring-cloud-consul-discovery/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file