From c6cc5617f397061362c811a780eba5c53f1da99e Mon Sep 17 00:00:00 2001 From: cumarav <142503314+cumarav@users.noreply.github.com> Date: Mon, 6 Nov 2023 12:19:12 +0100 Subject: [PATCH] feat: Cloud Gateway - create additional registrations (#3181) Signed-off-by: alexandr cumarav --- .github/workflows/integration-tests.yml | 11 ++ .../config/AdditionalEurekaClientsHolder.java | 34 +++++ .../config/ConnectionsConfig.java | 63 +++++++- .../config/EurekaFactory.java | 42 ++++++ .../src/main/resources/application.yml | 22 ++- .../src/main/resources/banner.txt | 8 + .../config/AdditionalRegistrationTest.java | 142 ++++++++++++++++++ .../config/ConnectionsConfigTest.java | 29 ++-- .../gateway/config/DiscoveryClientConfig.java | 2 +- gradle/versions.gradle | 2 + integration-tests/build.gradle | 1 + .../cloudgateway/CentralRegistryTest.java | 48 +++++- 12 files changed, 373 insertions(+), 31 deletions(-) create mode 100644 cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/AdditionalEurekaClientsHolder.java create mode 100644 cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/EurekaFactory.java create mode 100644 cloud-gateway-service/src/main/resources/banner.txt create mode 100644 cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/AdditionalRegistrationTest.java diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 2557065180..2507e5c520 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -274,6 +274,17 @@ jobs: ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_GATEWAYURL: / ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_SERVICEURL: / + cloud-gateway-service-2: + image: ghcr.io/balhar-jakub/cloud-gateway-service:${{ github.run_id }}-${{ github.run_number }} + env: + APIML_SERVICE_APIMLID: domain-apiml + APIML_SERVICE_HOSTNAME: cloud-gateway-service-2 + APIML_CLOUDGATEWAY_REGISTRY_ENABLED: false + APIML_SECURITY_X509_REGISTRY_ALLOWEDUSERS: USER,UNKNOWNUSER + APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10031/eureka/ + ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka + ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_GATEWAYURL: / + ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_SERVICEURL: / steps: - uses: actions/checkout@v3 diff --git a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/AdditionalEurekaClientsHolder.java b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/AdditionalEurekaClientsHolder.java new file mode 100644 index 0000000000..c481cba2d6 --- /dev/null +++ b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/AdditionalEurekaClientsHolder.java @@ -0,0 +1,34 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.cloudgatewayservice.config; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.cloud.netflix.eureka.CloudEurekaClient; + +import java.util.List; + +/** + * Purpose of this holder is to keep additional {@link CloudEurekaClient} instances in the custom bean so that it does not interfere with standard `primary` eureka autoconfiguration. + *

+ * Wrapper exposes proxy `shutdown` call of the client instances + */ +@Getter +@RequiredArgsConstructor +public class AdditionalEurekaClientsHolder { + private final List discoveryClients; + + public void shutdown() { + if (discoveryClients != null) { + discoveryClients.forEach(CloudEurekaClient::shutdown); + } + } +} diff --git a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java index 4e2a08c217..b1d019291d 100644 --- a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java +++ b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfig.java @@ -11,7 +11,9 @@ package org.zowe.apiml.cloudgatewayservice.config; import com.netflix.appinfo.ApplicationInfoManager; +import com.netflix.appinfo.EurekaInstanceConfig; import com.netflix.appinfo.HealthCheckHandler; +import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.AbstractDiscoveryClientOptionalArgs; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.EurekaClientConfig; @@ -22,6 +24,7 @@ import io.netty.handler.ssl.SslContextBuilder; import lombok.extern.slf4j.Slf4j; import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -34,16 +37,22 @@ import org.springframework.cloud.gateway.config.HttpClientCustomizer; import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping; import org.springframework.cloud.netflix.eureka.CloudEurekaClient; +import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean; import org.springframework.cloud.netflix.eureka.MutableDiscoveryClientOptionalArgs; import org.springframework.cloud.util.ProxyUtils; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.StandardEnvironment; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.cors.reactive.CorsConfigurationSource; import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.pattern.PathPatternParser; +import org.zowe.apiml.config.AdditionalRegistration; +import org.zowe.apiml.config.AdditionalRegistrationCondition; +import org.zowe.apiml.config.AdditionalRegistrationParser; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; @@ -59,6 +68,12 @@ import javax.net.ssl.TrustManagerFactory; import java.security.KeyStore; import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.springframework.cloud.netflix.eureka.EurekaClientConfigBean.DEFAULT_ZONE; @Configuration @@ -195,12 +210,56 @@ public CloudEurekaClient primaryEurekaClient(ApplicationInfoManager manager, Eur AbstractDiscoveryClientOptionalArgs args = new MutableDiscoveryClientOptionalArgs(); args.setEurekaJerseyClient(eurekaJerseyClient); - CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, config, args, - this.context); + final CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, config, args, this.context); cloudEurekaClient.registerHealthCheck(healthCheckHandler); return cloudEurekaClient; } + @Bean + public List additionalRegistration(StandardEnvironment environment) { + List additionalRegistrations = new AdditionalRegistrationParser().extractAdditionalRegistrations(System.getenv()); + log.debug("Parsed {} additional registration: {}", additionalRegistrations.size(), additionalRegistrations); + return additionalRegistrations; + } + + @Bean(destroyMethod = "shutdown") + @Conditional(AdditionalRegistrationCondition.class) + @RefreshScope + public AdditionalEurekaClientsHolder additionalEurekaClientsHolder(ApplicationInfoManager manager, + EurekaClientConfig config, + List additionalRegistrations, + EurekaFactory eurekaFactory, + @Autowired(required = false) HealthCheckHandler healthCheckHandler + ) { + List additionalClients = new ArrayList<>(additionalRegistrations.size()); + for (AdditionalRegistration apimlRegistration : additionalRegistrations) { + CloudEurekaClient cloudEurekaClient = registerInTheApimlInstance(config, apimlRegistration, manager, eurekaFactory); + additionalClients.add(cloudEurekaClient); + cloudEurekaClient.registerHealthCheck(healthCheckHandler); + } + return new AdditionalEurekaClientsHolder(additionalClients); + } + + private CloudEurekaClient registerInTheApimlInstance(EurekaClientConfig config, AdditionalRegistration apimlRegistration, ApplicationInfoManager appManager, EurekaFactory eurekaFactory) { + + log.debug("additional registration: {}", apimlRegistration.getDiscoveryServiceUrls()); + Map urls = new HashMap<>(); + urls.put(DEFAULT_ZONE, apimlRegistration.getDiscoveryServiceUrls()); + + EurekaClientConfigBean configBean = new EurekaClientConfigBean(); + BeanUtils.copyProperties(config, configBean); + configBean.setServiceUrl(urls); + + EurekaJerseyClient jerseyClient = factory().createEurekaJerseyClientBuilder(eurekaServerUrl, serviceId).build(); + MutableDiscoveryClientOptionalArgs args = new MutableDiscoveryClientOptionalArgs(); + args.setEurekaJerseyClient(jerseyClient); + + EurekaInstanceConfig eurekaInstanceConfig = appManager.getEurekaInstanceConfig(); + InstanceInfo newInfo = eurekaFactory.createInstanceInfo(eurekaInstanceConfig); + + return eurekaFactory.createCloudEurekaClient(eurekaInstanceConfig, newInfo, configBean, args, context); + } + @Bean public Customizer defaultCustomizer() { return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id) diff --git a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/EurekaFactory.java b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/EurekaFactory.java new file mode 100644 index 0000000000..d9541ab27b --- /dev/null +++ b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/EurekaFactory.java @@ -0,0 +1,42 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.cloudgatewayservice.config; + +import com.netflix.appinfo.ApplicationInfoManager; +import com.netflix.appinfo.EurekaInstanceConfig; +import com.netflix.appinfo.InstanceInfo; +import org.springframework.cloud.netflix.eureka.CloudEurekaClient; +import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean; +import org.springframework.cloud.netflix.eureka.InstanceInfoFactory; +import org.springframework.cloud.netflix.eureka.MutableDiscoveryClientOptionalArgs; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * Eureka dependencies injection helper + */ +@Component +public class EurekaFactory { + + /** + * Create new copy of instance info + * + * @param instanceConfig eureka instance config to copy from + */ + InstanceInfo createInstanceInfo(EurekaInstanceConfig instanceConfig) { + return new InstanceInfoFactory().create(instanceConfig); + } + + public CloudEurekaClient createCloudEurekaClient(EurekaInstanceConfig eurekaInstanceConfig, InstanceInfo newInfo, EurekaClientConfigBean configBean, MutableDiscoveryClientOptionalArgs args, ApplicationContext context) { + ApplicationInfoManager perClientAppManager = new ApplicationInfoManager(eurekaInstanceConfig, newInfo, null); + return new CloudEurekaClient(perClientAppManager, configBean, args, context); + } +} diff --git a/cloud-gateway-service/src/main/resources/application.yml b/cloud-gateway-service/src/main/resources/application.yml index 8a108bdcb0..bf7a28e40d 100644 --- a/cloud-gateway-service/src/main/resources/application.yml +++ b/cloud-gateway-service/src/main/resources/application.yml @@ -1,8 +1,20 @@ eureka: + instance: + instanceId: ${apiml.service.hostname}:${apiml.service.id}:${apiml.service.port} + #ports are computed in code + homePageUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/ + healthCheckUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/application/health + metadata-map: + apiml: + service.apimlId : ${apiml.service.apimlId} client: + fetchRegistry: true + registerWithEureka: true region: default serviceUrl: defaultZone: ${apiml.service.discoveryServiceUrls} + healthcheck: + enabled: true spring: application: @@ -13,14 +25,14 @@ apiml: timeout: 60 service: apimlId: apiml1 + corsEnabled: true + discoveryServiceUrls: https://localhost:10011/eureka/ + forwardClientCertEnabled: false + hostname: localhost id: ${spring.application.name} + ignoredHeadersWhenCorsEnabled: Access-Control-Request-Method,Access-Control-Request-Headers,Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Credentials,Origin port: 10023 - hostname: localhost - discoveryServiceUrls: https://localhost:10011/eureka/ scheme: https # "https" or "http" - corsEnabled: true - ignoredHeadersWhenCorsEnabled: Access-Control-Request-Method,Access-Control-Request-Headers,Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Credentials,Origin - forwardClientCertEnabled: false security: ssl: nonStrictVerifySslCertificatesOfServices: true diff --git a/cloud-gateway-service/src/main/resources/banner.txt b/cloud-gateway-service/src/main/resources/banner.txt new file mode 100644 index 0000000000..6a731a8ee5 --- /dev/null +++ b/cloud-gateway-service/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + _____ _ _ _____ _ + / ____|| | | | / ____| | | + | | | | ___ _ _ __| | | | __ __ _ | |_ ___ __ __ __ _ _ _ + | | | | / _ \ | | | | / _` | | | |_ | / _` || __|/ _ \\ \ /\ / // _` || | | | + | |____ | || (_) || |_| || (_| | | |__| || (_| || |_| __/ \ V V /| (_| || |_| | + \_____||_| \___/ \__,_| \__,_| \_____| \__,_| \__|\___| \_/\_/ \__,_| \__, | + __/ | + |___/ diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/AdditionalRegistrationTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/AdditionalRegistrationTest.java new file mode 100644 index 0000000000..92e578d469 --- /dev/null +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/AdditionalRegistrationTest.java @@ -0,0 +1,142 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.cloudgatewayservice.config; + +import com.netflix.appinfo.ApplicationInfoManager; +import com.netflix.appinfo.EurekaInstanceConfig; +import com.netflix.appinfo.HealthCheckHandler; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.EurekaClientConfig; +import com.netflix.discovery.shared.transport.jersey.EurekaJerseyClientImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.cloud.netflix.eureka.CloudEurekaClient; +import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean; +import org.springframework.context.ApplicationContext; +import org.zowe.apiml.config.AdditionalRegistration; +import org.zowe.apiml.security.HttpsFactory; + +import java.util.AbstractMap; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.cloud.netflix.eureka.EurekaClientConfigBean.DEFAULT_ZONE; + +@ExtendWith(MockitoExtension.class) +public class AdditionalRegistrationTest { + + private ConnectionsConfig connectionsConfig; + @Mock + private CloudEurekaClient additionalClientOne; + @Mock + private CloudEurekaClient additionalClientTwo; + @Mock + private EurekaFactory eurekaFactory; + @Mock + ApplicationContext context; + + @BeforeEach + void setUp() { + connectionsConfig = new ConnectionsConfig(context); + } + + @ExtendWith(MockitoExtension.class) + @Nested + class WhenInitializingAdditionalRegistrations { + private ConnectionsConfig configSpy; + @Mock + private ApplicationInfoManager manager; + @Mock + private EurekaClientConfig clientConfig; + @Mock + private HealthCheckHandler healthCheckHandler; + @Mock + private HttpsFactory httpsFactory; + + @Captor + private ArgumentCaptor clientConfigCaptor; + + private final AdditionalRegistration registration = AdditionalRegistration.builder().discoveryServiceUrls("https://another-eureka-1").build(); + + @BeforeEach + public void setUp() { + configSpy = Mockito.spy(connectionsConfig); + lenient().doReturn(httpsFactory).when(configSpy).factory(); + lenient().when(httpsFactory.createEurekaJerseyClientBuilder(any(), any())).thenReturn(mock(EurekaJerseyClientImpl.EurekaJerseyClientBuilder.class)); + + lenient().when(eurekaFactory.createCloudEurekaClient(any(), any(), clientConfigCaptor.capture(), any(), any())).thenReturn(additionalClientOne, additionalClientTwo); + } + + @Test + void shouldCreateEurekaClientForAdditionalDiscoveryUrl() { + + AdditionalEurekaClientsHolder holder = configSpy.additionalEurekaClientsHolder(manager, clientConfig, singletonList(registration), eurekaFactory, healthCheckHandler); + + assertThat(holder.getDiscoveryClients()).hasSize(1); + EurekaClientConfigBean eurekaClientConfigBean = clientConfigCaptor.getValue(); + assertThat(eurekaClientConfigBean.getServiceUrl()).containsOnly(new AbstractMap.SimpleEntry(DEFAULT_ZONE, "https://another-eureka-1")); + } + + @Test + void shouldCreateTwoAdditionalRegistrations() { + AdditionalRegistration secondRegistration = AdditionalRegistration.builder().discoveryServiceUrls("https://another-eureka-2").build(); + AdditionalEurekaClientsHolder holder = configSpy.additionalEurekaClientsHolder(manager, clientConfig, asList(registration, secondRegistration), eurekaFactory, healthCheckHandler); + + assertThat(holder.getDiscoveryClients()).hasSize(2); + verify(additionalClientOne).registerHealthCheck(healthCheckHandler); + verify(additionalClientTwo).registerHealthCheck(healthCheckHandler); + } + + @Test + void shouldCreateInstanceInfoFromEurekaConfig() { + EurekaInstanceConfig config = mock(EurekaInstanceConfig.class); + when(config.getNamespace()).thenReturn(""); + when(config.getAppname()).thenReturn("CLOUD-GATEWAY"); + + InstanceInfo instanceInfo = new EurekaFactory().createInstanceInfo(config); + + assertThat(instanceInfo.getAppName()).isEqualTo("CLOUD-GATEWAY"); + } + } + + @Nested + class WhenClientHolderShutDown { + @Test + void shouldTriggerShutdownCallToWrappedClients() { + AdditionalEurekaClientsHolder holder = new AdditionalEurekaClientsHolder(asList(additionalClientOne, additionalClientTwo)); + holder.shutdown(); + + verify(additionalClientOne).shutdown(); + verify(additionalClientTwo).shutdown(); + } + + @Test + void shouldHandleNullsOnShutdownCall() { + AdditionalEurekaClientsHolder holder = new AdditionalEurekaClientsHolder(null); + holder.shutdown(); + + assertThat(holder.getDiscoveryClients()).isNull(); + } + } +} diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfigTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfigTest.java index 219abf0ec1..fcf6b3c7ba 100644 --- a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfigTest.java +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/config/ConnectionsConfigTest.java @@ -14,7 +14,6 @@ import com.netflix.appinfo.HealthCheckHandler; import com.netflix.discovery.EurekaClientConfig; import com.netflix.discovery.shared.transport.jersey.EurekaJerseyClient; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -23,9 +22,7 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.test.util.ReflectionTestUtils; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest @ComponentScan(basePackages = "org.zowe.apiml.cloudgatewayservice") @@ -40,8 +37,8 @@ class ConnectionsConfigTest { class WhenCreateEurekaJerseyClientBuilder { @Test void thenIsNotNull() { - Assertions.assertNotNull(connectionsConfig); - Assertions.assertNotNull(connectionsConfig.getEurekaJerseyClient()); + assertThat(connectionsConfig).isNotNull(); + assertThat(connectionsConfig.getEurekaJerseyClient()).isNotNull(); } } @@ -61,7 +58,7 @@ class WhenInitializeEurekaClient { @Test void thenCreateIt() { - Assertions.assertNotNull(connectionsConfig.primaryEurekaClient(manager, config, eurekaJerseyClient, healthCheckHandler)); + assertThat(connectionsConfig.primaryEurekaClient(manager, config, eurekaJerseyClient, healthCheckHandler)).isNotNull(); } } @@ -76,10 +73,10 @@ void whenKeyringHasWrongFormatAndMissingPasswords_thenFixIt() { connectionsConfig.updateConfigParameters(); - assertEquals("safkeyring://userId/ringId1", ReflectionTestUtils.getField(connectionsConfig, "keyStorePath")); - assertEquals("safkeyring://userId/ringId2", ReflectionTestUtils.getField(connectionsConfig, "trustStorePath")); - assertArrayEquals("password".toCharArray(), (char[]) ReflectionTestUtils.getField(connectionsConfig, "keyStorePassword")); - assertArrayEquals("password".toCharArray(), (char[]) ReflectionTestUtils.getField(connectionsConfig, "trustStorePassword")); + assertThat(ReflectionTestUtils.getField(connectionsConfig, "keyStorePath")).isEqualTo("safkeyring://userId/ringId1"); + assertThat(ReflectionTestUtils.getField(connectionsConfig, "trustStorePath")).isEqualTo("safkeyring://userId/ringId2"); + assertThat((char[]) ReflectionTestUtils.getField(connectionsConfig, "keyStorePassword")).isEqualTo("password".toCharArray()); + assertThat((char[]) ReflectionTestUtils.getField(connectionsConfig, "trustStorePassword")).isEqualTo("password".toCharArray()); } @Test @@ -90,13 +87,11 @@ void whenKeystore_thenDoNothing() { connectionsConfig.updateConfigParameters(); - assertEquals("/path1", ReflectionTestUtils.getField(connectionsConfig, "keyStorePath")); - assertEquals("/path2", ReflectionTestUtils.getField(connectionsConfig, "trustStorePath")); - assertNull(ReflectionTestUtils.getField(connectionsConfig, "keyStorePassword")); - assertNull(ReflectionTestUtils.getField(connectionsConfig, "trustStorePassword")); + assertThat(ReflectionTestUtils.getField(connectionsConfig, "keyStorePath")).isEqualTo("/path1"); + assertThat(ReflectionTestUtils.getField(connectionsConfig, "trustStorePath")).isEqualTo("/path2"); + assertThat(ReflectionTestUtils.getField(connectionsConfig, "keyStorePassword")).isNull(); + assertThat(ReflectionTestUtils.getField(connectionsConfig, "trustStorePassword")).isNull(); } - } - } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/DiscoveryClientConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/DiscoveryClientConfig.java index 6b2f458b48..72ca7173ad 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/DiscoveryClientConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/DiscoveryClientConfig.java @@ -67,7 +67,7 @@ public class DiscoveryClientConfig { @Bean public List additionalRegistration(StandardEnvironment environment) { List additionalRegistrations = new AdditionalRegistrationParser().extractAdditionalRegistrations(System.getenv()); - log.debug("Parsed {} additional regs, \t first: {}", additionalRegistrations.size(), additionalRegistrations.stream().findFirst().orElse(null)); + log.debug("Parsed {} additional registration: {}", additionalRegistrations.size(), additionalRegistrations); return additionalRegistrations; } diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 0b01fede5f..51afd97939 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -153,6 +153,7 @@ dependencyResolutionManagement { version('checkstyle', '9.3') version('jacoco', '0.8.10') version('gradle', '8.1.1') + version('assertjCore', '3.24.2') library('zowe_attls', 'org.zowe.apiml.sdk', 'attls').versionRef('attls') library('spring_boot_configuration_processor', 'org.springframework.boot', 'spring-boot-configuration-processor').versionRef('springBoot') @@ -350,6 +351,7 @@ dependencyResolutionManagement { library('woodstox_core', 'com.fasterxml.woodstox', 'woodstox-core').versionRef('woodstoxCore') library('woodstox_stax2', 'org.codehaus.woodstox', 'stax2-api').versionRef('woodstoxStax2') library('xstream', 'com.thoughtworks.xstream', 'xstream').versionRef('xstream') + library('assertj_core','org.assertj', 'assertj-core').versionRef('assertjCore') // Sample apps only library('jersey_container_servlet_core', 'org.glassfish.jersey.containers', 'jersey-container-servlet-core').versionRef('jersey') diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index d90b58bce7..0316339f66 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -46,6 +46,7 @@ dependencies { testImplementation libs.jackson.databind testImplementation libs.jackson.dataformat.yaml testImplementation libs.javax.servlet.api + testImplementation libs.assertj.core testImplementation libs.nimbusJoseJwt runtimeOnly libs.jjwt.impl runtimeOnly libs.jjwt.jackson diff --git a/integration-tests/src/test/java/org/zowe/apiml/functional/cloudgateway/CentralRegistryTest.java b/integration-tests/src/test/java/org/zowe/apiml/functional/cloudgateway/CentralRegistryTest.java index 5aad09e781..5064eb2220 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/functional/cloudgateway/CentralRegistryTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/functional/cloudgateway/CentralRegistryTest.java @@ -15,7 +15,6 @@ import io.restassured.http.ContentType; import io.restassured.response.ValidatableResponse; import lombok.SneakyThrows; -import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; @@ -26,21 +25,25 @@ import org.zowe.apiml.util.categories.DiscoverableClientDependentTest; import org.zowe.apiml.util.config.CloudGatewayConfiguration; import org.zowe.apiml.util.config.ConfigReader; +import org.zowe.apiml.util.config.DiscoveryServiceConfiguration; import org.zowe.apiml.util.config.SslContext; import org.zowe.apiml.util.config.SslContextConfigurer; import org.zowe.apiml.util.config.TlsConfiguration; import java.net.URI; import java.net.URL; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static com.google.common.base.Strings.nullToEmpty; import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.with; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasSize; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; +import static org.springframework.http.HttpHeaders.ACCEPT; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @DiscoverableClientDependentTest @Tag("CloudGatewayCentralRegistry") @@ -48,12 +51,13 @@ class CentralRegistryTest implements TestWithStartedInstances { static final String CENTRAL_REGISTRY_PATH = "/" + CoreService.CLOUD_GATEWAY.getServiceId() + "/api/v1/registry/"; static CloudGatewayConfiguration conf = ConfigReader.environmentConfiguration().getCloudGatewayConfiguration(); + static DiscoveryServiceConfiguration discoveryConf = ConfigReader.environmentConfiguration().getDiscoveryServiceConfiguration(); @BeforeAll @SneakyThrows static void setupAll() { //In order to avoid config customization - ConfigReader.environmentConfiguration().getGatewayServiceConfiguration().setInstances(1); + ConfigReader.environmentConfiguration().getGatewayServiceConfiguration().setInstances(2); TlsConfiguration tlsCfg = ConfigReader.environmentConfiguration().getTlsConfiguration(); SslContextConfigurer sslContextConfigurer = new SslContextConfigurer(tlsCfg.getKeyStorePassword(), tlsCfg.getClientKeystore(), tlsCfg.getKeyStore()); @@ -73,7 +77,7 @@ void shouldFindRegisteredGatewayInCentralApiml() { List> services = response.extract().jsonPath().getObject("[0].services", new TypeRef>>() { }); - assertThat(services, hasSize(1)); + assertThat(services).hasSize(1); } @Test @@ -87,7 +91,24 @@ void shouldFindBothApimlIds() { List apimlIds = listCentralRegistry(null, null, null) .extract().jsonPath().getList("apimlId"); - assertThat(apimlIds, Matchers.containsInAnyOrder("central-apiml", "domain-apiml")); + assertThat(apimlIds).contains("central-apiml", "domain-apiml"); + } + + @Test + void shouldFindTwoRegisteredCloudGatewaysInTheEurekaApps() { + TypeRef>>> typeRef = new TypeRef>>>() { + }; + + ArrayList> metadata = listEurekaApps() + .extract() + .jsonPath() + .getObject("applications.application.findAll { it.name == 'CLOUD-GATEWAY' }.instance.metadata", typeRef).get(0); + + assertThat(metadata).hasSize(2); + + assertThat(metadata) + .extracting(map -> map.get("apiml.service.apimlId")) + .containsExactlyInAnyOrder("central-apiml", "domain-apiml"); } @Test @@ -121,6 +142,21 @@ private ValidatableResponse listCentralRegistry(String apimlId, String apiId, St .contentType(ContentType.JSON); } + @SneakyThrows + private ValidatableResponse listEurekaApps() { + + URI eurekaApps = new URL(discoveryConf.getScheme(), discoveryConf.getHost(), discoveryConf.getPort(), "/eureka/apps") + .toURI(); + + return with().given() + .config(SslContext.clientCertUser) + .header(ACCEPT, APPLICATION_JSON_VALUE) + .get(eurekaApps) + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } + @SneakyThrows private URI buildRegistryURI(String apimlId, String apiId, String serviceId) {