diff --git a/cloud-gateway-service/build.gradle b/cloud-gateway-service/build.gradle index 32c7946499..40f87fef6e 100644 --- a/cloud-gateway-service/build.gradle +++ b/cloud-gateway-service/build.gradle @@ -60,6 +60,8 @@ configurations.all { dependencies { api project(':common-service-core') + api project(':apiml-utility') + implementation libs.eh.cache implementation libs.spring.cloud.starter.gateway @@ -80,6 +82,7 @@ dependencies { implementation libs.spring.expression implementation libs.bcpkix implementation libs.nimbusJoseJwt + implementation libs.janino compileOnly libs.lombok annotationProcessor libs.lombok @@ -89,8 +92,9 @@ dependencies { testAnnotationProcessor libs.lombok testImplementation libs.spring.boot.starter.test testImplementation libs.rest.assured - + testImplementation libs.reactorTest testImplementation libs.mockito.inline + } bootJar { 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 3c88aff37c..07516793f3 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 @@ -45,8 +45,10 @@ import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.pattern.PathPatternParser; import org.zowe.apiml.message.core.MessageService; +import org.zowe.apiml.message.log.ApimlLogger; import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; import org.zowe.apiml.security.HttpsConfig; +import org.zowe.apiml.security.HttpsConfigError; import org.zowe.apiml.security.HttpsFactory; import org.zowe.apiml.security.SecurityUtils; import org.zowe.apiml.util.CorsUtils; @@ -112,6 +114,7 @@ public class ConnectionsConfig { @Value("${apiml.service.corsEnabled:false}") private boolean corsEnabled; private final ApplicationContext context; + private static final ApimlLogger apimlLog = ApimlLogger.of(ConnectionsConfig.class, YamlMessageServiceInstance.getInstance()); public ConnectionsConfig(ApplicationContext context) { this.context = context; @@ -171,9 +174,9 @@ SslContext sslContext() { trustManagerFactory.init(trustStore); return SslContextBuilder.forClient().keyManager(keyManagerFactory).trustManager(trustManagerFactory).build(); } catch (Exception e) { - log.error("Exception while creating SSL context", e); - System.exit(1); - return null; + apimlLog.log("org.zowe.apiml.common.sslContextInitializationError", e.getMessage()); + throw new HttpsConfigError("Error initializing SSL Context: " + e.getMessage(), e, + HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED, factory().getConfig()); } } diff --git a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/RegistryConfig.java b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/RegistryConfig.java new file mode 100644 index 0000000000..7f97b849ee --- /dev/null +++ b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/config/RegistryConfig.java @@ -0,0 +1,31 @@ +/* + * 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.discovery.EurekaClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.zowe.apiml.services.BasicInfoService; +import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; + +@Configuration +public class RegistryConfig { + + @Bean + public EurekaMetadataParser eurekaMetadataParser() { + return new EurekaMetadataParser(); + } + + @Bean + public BasicInfoService basicInfoService(EurekaClient eurekaClient, EurekaMetadataParser eurekaMetadataParser) { + return new BasicInfoService(eurekaClient, eurekaMetadataParser); + } +} diff --git a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/scheduled/GatewayScanJob.java b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/scheduled/GatewayScanJob.java new file mode 100644 index 0000000000..ec3bdf7ac7 --- /dev/null +++ b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/scheduled/GatewayScanJob.java @@ -0,0 +1,95 @@ +/* + * 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.scheduled; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaRegistration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.zowe.apiml.cloudgatewayservice.service.GatewayIndexService; +import org.zowe.apiml.cloudgatewayservice.service.InstanceInfoService; +import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.services.BasicInfoService; +import org.zowe.apiml.services.ServiceInfo; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.zowe.apiml.constants.EurekaMetadataDefinition.APIML_ID; + +/** + * Scheduled job to refresh registry of all registered gateways services. + * Behaviour of the job can be configured by the following settings: + *
+ *   apiml:
+ *     cloudGateway:
+ *       cachePeriodSec: - default value 120 seconds
+ *       maxSimultaneousRequests:  - default value 20
+ *       clientKeystore: - default value null
+ *       clientKeystorePassword: - default value null
+ *       clientKeystoreType: - default PKCS12
+ *       serviceRegistryEnabled: - default value false
+ * 
+ */ +@EnableScheduling +@Slf4j +@Component +@ConditionalOnExpression("${apiml.cloudGateway.serviceRegistryEnabled:false}") +@RequiredArgsConstructor +public class GatewayScanJob { + + private final BasicInfoService basicInfoService; + private final EurekaRegistration serviceRegistration; + private final GatewayIndexService gatewayIndexerService; + private final InstanceInfoService instanceInfoService; + @Value("${apiml.service.apimlId:#{null}}") + private String currentApimlId; + @Value("${apiml.cloudGateway.maxSimultaneousRequests:20}") + private int maxSimultaneousRequests; + + @Scheduled(initialDelay = 5000, fixedDelayString = "${apiml.cloudGateway.refresh-interval-ms:30000}") + public void startScanExternalGatewayJob() { + + log.debug("Scan gateways job start"); + doScanExternalGateway() + .subscribe(); + addLocalServices(); + } + + private void addLocalServices() { + String apimlIdKey = Optional.ofNullable(currentApimlId).orElse(serviceRegistration.getInstanceId()); + List localServices = basicInfoService.getServicesInfo(); + gatewayIndexerService.putApimlServices(apimlIdKey, localServices); + } + + /** + * reactive entry point for the external gateways index refresh + */ + protected Flux> doScanExternalGateway() { + + Mono> registeredGateways = instanceInfoService.getServiceInstance(CoreService.GATEWAY.getServiceId()) + .map(gateways -> gateways.stream().filter(info -> !StringUtils.equals(info.getMetadata().getOrDefault(APIML_ID, "N/A"), currentApimlId)).collect(Collectors.toList())); + + Flux serviceInstanceFlux = registeredGateways.flatMapMany(Flux::fromIterable); + + return serviceInstanceFlux + .flatMap(gatewayIndexerService::indexGatewayServices, maxSimultaneousRequests); + } +} diff --git a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/GatewayIndexService.java b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/GatewayIndexService.java new file mode 100644 index 0000000000..0356dd5ac2 --- /dev/null +++ b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/GatewayIndexService.java @@ -0,0 +1,170 @@ +/* + * 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.service; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableMap; +import io.netty.handler.ssl.SslContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.web.reactive.function.client.WebClient; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; +import org.zowe.apiml.services.ServiceInfo; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import reactor.netty.tcp.SslProvider; + +import javax.validation.constraints.NotNull; +import java.util.AbstractMap; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import static java.util.Objects.nonNull; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.apache.commons.lang.StringUtils.isNotBlank; +import static org.springframework.util.CollectionUtils.isEmpty; +import static org.zowe.apiml.cloudgatewayservice.service.WebClientHelper.load; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.APIML_ID; + +/** + * Maintain all registered gateways lookup data. Internal caches uses apimlId is a key. + * if apimlId key is not available then synthetic key is generated containing SUBSTITUTE_ prefix and instanceId + */ +@Slf4j +@Service +public class GatewayIndexService { + private final ApimlLogger apimlLog = ApimlLogger.of(GatewayIndexService.class, YamlMessageServiceInstance.getInstance()); + private final Cache apimlGatewayLookup; + private final Cache> apimlServicesCache; + private final WebClient defaultWebClient; + private SslContext customClientSslContext = null; + + public GatewayIndexService(WebClient defaultWebClient, + @Value("${apiml.cloudGateway.cachePeriodSec:120}") int cachePeriodSec, + @Value("${apiml.cloudGateway.clientKeystore:#{null}}") String clientKeystorePath, + @Value("${apiml.cloudGateway.clientKeystorePassword:#{null}}") char[] clientKeystorePassword, + @Value("${apiml.cloudGateway.clientKeystoreType:PKCS12}") String keystoreType + ) { + this.defaultWebClient = defaultWebClient; + + apimlGatewayLookup = CacheBuilder.newBuilder().expireAfterWrite(cachePeriodSec, SECONDS).build(); + apimlServicesCache = CacheBuilder.newBuilder().expireAfterWrite(cachePeriodSec, SECONDS).build(); + + if (isNotBlank(clientKeystorePath) && nonNull(clientKeystorePassword)) { + customClientSslContext = load(clientKeystorePath, clientKeystorePassword, keystoreType); + } + } + + private WebClient buildWebClient(ServiceInstance registration) { + final String baseUrl = String.format("%s://%s:%d", registration.getScheme(), registration.getHost(), registration.getPort()); + if (this.customClientSslContext != null) { + SslProvider sslProvider = SslProvider.builder().sslContext(customClientSslContext).build(); + HttpClient httpClient = HttpClient.create() + .secure(sslProvider); + + return WebClient.builder() + .baseUrl(baseUrl) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .defaultHeader("Accept", MediaType.APPLICATION_JSON_VALUE) + .build(); + } + + return defaultWebClient.mutate() + .baseUrl(baseUrl) + .build(); + } + + public Mono> indexGatewayServices(ServiceInstance registration) { + String apimlIdKey = extractApimlId(registration).orElse(buildAlternativeApimlIdKey(registration)); + log.debug("Fetching registered gateway instance services: {}", apimlIdKey); + apimlGatewayLookup.put(apimlIdKey, registration); + return fetchServices(apimlIdKey, registration) + .doOnError(ex -> apimlLog.log("org.zowe.apiml.gateway.servicesRequestFailed", apimlIdKey, ex.getMessage())) + .onErrorComplete() + .doFinally(signal -> log.debug("\t {} completed with {}", apimlIdKey, signal)); + } + + /** + * Store entry in the Services Registry. Should be used to store services info from the current apiml instance + * + * @param apimlId unique apimlId + * @param services List of the services + */ + public void putApimlServices(@NotNull String apimlId, List services) { + apimlServicesCache.put(apimlId, services); + } + + private Mono> fetchServices(String apimlId, ServiceInstance registration) { + WebClient webClient = buildWebClient(registration); + final ParameterizedTypeReference> serviceInfoType = new ParameterizedTypeReference>() { + }; + + return webClient.get().uri("/gateway/services") + .retrieve() + .bodyToMono(serviceInfoType) + .doOnNext(foreignServices -> apimlServicesCache.put(apimlId, foreignServices)); + } + + private String buildAlternativeApimlIdKey(ServiceInstance registration) { + return "SUBSTITUTE" + "_" + registration.getInstanceId(); + } + + private Optional extractApimlId(ServiceInstance registration) { + return Optional.ofNullable(registration.getMetadata()).map(m -> m.get(APIML_ID)); + } + + /** + * list currently cached apiml registry with option to filter by the apimlId and apiId + * + * @param apimlId - filter for only services from the particular apiml instance, NULL - filter not applied + * @param apiId - filter for only services of particular type e.g. zowe.apiml.apicatalog + * @return full of filter immutable map of the registry + */ + public Map> listRegistry(String apimlId, String apiId) { + + Map> allServices = ImmutableMap.>builder() + .putAll(apimlServicesCache.asMap()).build(); + return allServices.entrySet().stream() + .filter(entry -> apimlId == null || StringUtils.equals(apimlId, entry.getKey())) + .map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(), filterServicesByApiId(entry.getValue(), apiId))) + .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); + } + + List filterServicesByApiId(List apimlIdServices, String apiId) { + if (!CollectionUtils.isEmpty(apimlIdServices)) { + return apimlIdServices.stream() + .filter(Objects::nonNull) + .filter(serviceInfo -> apiId == null || hasSameApiId(serviceInfo, apiId)) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + private boolean hasSameApiId(ServiceInfo serviceInfo, String apiId) { + if (serviceInfo.getApiml() != null && !isEmpty(serviceInfo.getApiml().getApiInfo())) { + return StringUtils.equals(apiId, serviceInfo.getApiml().getApiInfo().get(0).getApiId()); + } + return false; + } +} diff --git a/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/WebClientHelper.java b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/WebClientHelper.java new file mode 100644 index 0000000000..c3e0cd86cb --- /dev/null +++ b/cloud-gateway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/WebClientHelper.java @@ -0,0 +1,79 @@ +/* + * 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.service; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.message.yaml.YamlMessageServiceInstance; +import org.zowe.apiml.security.HttpsConfigError; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLException; +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; + +/** + * Utility class for the custom Netty {@link SslContext} creation. + * Does not support keyring because client keystore override mainly used in development mode and not supposed be run on the Mainframe. + */ +@Slf4j +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class WebClientHelper { + private static final ApimlLogger apimlLog = ApimlLogger.of(WebClientHelper.class, YamlMessageServiceInstance.getInstance()); + + /** + * Load {@link SslContext} from the specified keystore + * + * @param keystorePath path to the keystore file + * @param password keystore password + * @param keystoreType keystore type + * @throws IllegalArgumentException if keystore file does not exist. + * @throws HttpsConfigError if any error occur during the context creation. + */ + public static SslContext load(String keystorePath, char[] password, String keystoreType) { + File keyStoreFile = new File(keystorePath); + if (keyStoreFile.exists()) { + try (InputStream is = Files.newInputStream(Paths.get(keystorePath))) { + KeyStore keyStore = KeyStore.getInstance(keystoreType); + keyStore.load(is, password); + return initSslContext(keyStore, password); + } catch (Exception e) { + apimlLog.log("org.zowe.apiml.common.sslContextInitializationError", e.getMessage()); + throw new HttpsConfigError("Error initializing SSL Context: " + e.getMessage(), e, + HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED); + } + } else { + throw new IllegalArgumentException("Not existing file: " + keystorePath); + } + } + + private static SslContext initSslContext(KeyStore keyStore, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, SSLException { + KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX"); + kmf.init(keyStore, password); + + return SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .keyManager(kmf).build(); + } + + +} diff --git a/cloud-gateway-service/src/main/resources/application.yml b/cloud-gateway-service/src/main/resources/application.yml index f6b7314cb7..c09d47f0dc 100644 --- a/cloud-gateway-service/src/main/resources/application.yml +++ b/cloud-gateway-service/src/main/resources/application.yml @@ -3,19 +3,28 @@ eureka: serviceUrl: defaultZone: https://localhost:10011/eureka/ +spring: + application: + name: cloud-gateway + apiml: gateway: timeout: 60 service: - id: cloud-gateway + apimlId: apiml1 + id: ${spring.application.name} port: 10023 hostname: localhost + 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 + cloudGateway: + serviceRegistryEnabled: false + server: port: ${apiml.service.port} ssl: @@ -29,7 +38,6 @@ server: trustStorePassword: password trustStoreType: PKCS12 -spring: cloud: gateway: discovery: @@ -41,10 +49,13 @@ spring: main: allow-circular-references: true +logbackServiceName: ZWEACG1 + logging: level: org.springframework.cloud.gateway: DEBUG reactor.netty.http.client: DEBUG + reactor.netty.http.client.HttpClientConnect: OFF management: endpoint: diff --git a/cloud-gateway-service/src/main/resources/cloud-gateway-log-messages.yml b/cloud-gateway-service/src/main/resources/cloud-gateway-log-messages.yml index 2835245a8e..6eddb131fa 100644 --- a/cloud-gateway-service/src/main/resources/cloud-gateway-log-messages.yml +++ b/cloud-gateway-service/src/main/resources/cloud-gateway-log-messages.yml @@ -351,6 +351,16 @@ messages: reason: "The JWT token or client certificate is not valid" action: "Configure your client to provide valid authentication." + - key: org.zowe.apiml.common.sslContextInitializationError + number: ZWEAM400 + type: ERROR + text: "Error initializing SSL Context: '%s'" + reason: "An error occurred while initializing the SSL Context." + action: "Refer to the specific message to identify the exact problem.\n + Possible causes include:\n + - Incorrect security algorithm\n + - The keystore is invalid or corrupted\n + - The certificate is invalid or corrupted" # Revoke personal access token - key: org.zowe.apiml.security.query.invalidRevokeRequestBody @@ -359,3 +369,10 @@ messages: text: "Body in the revoke request is not valid." reason: "The request body is not valid" action: "Use a valid body in the request. Format of a message: {userId: string, (optional)timestamp: long} or {serviceId: string, (optional)timestamp: long}." + + - key: org.zowe.apiml.gateway.servicesRequestFailed + number: ZWESG100 + type: WARNING + text: "Cannot receive information about services on API Gateway with apimlId '%s' because: %s" + reason: "Cannot connect to the Gateway service." + action: "Make sure that the external Gateway service is running and the truststore of the both Gateways contain the corresponding certificate." diff --git a/cloud-gateway-service/src/main/resources/logback.xml b/cloud-gateway-service/src/main/resources/logback.xml new file mode 100644 index 0000000000..999077ccc1 --- /dev/null +++ b/cloud-gateway-service/src/main/resources/logback.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + ${apimlLogPattern} + + + + + + ${STORAGE_LOCATION}/${logbackServiceName}.log + + + ${STORAGE_LOCATION}/${logbackServiceName}.%i.log + ${MIN_INDEX} + ${MAX_INDEX} + + + + ${MAX_FILE_SIZE} + + + + + ${apimlLogPattern} + + + + + + + + + \ No newline at end of file diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/scheduled/GatewayScanJobTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/scheduled/GatewayScanJobTest.java new file mode 100644 index 0000000000..41525558ec --- /dev/null +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/scheduled/GatewayScanJobTest.java @@ -0,0 +1,99 @@ +/* + * 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.scheduled; + + +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaRegistration; +import org.springframework.test.util.ReflectionTestUtils; +import org.zowe.apiml.cloudgatewayservice.service.GatewayIndexService; +import org.zowe.apiml.cloudgatewayservice.service.InstanceInfoService; +import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.services.BasicInfoService; +import org.zowe.apiml.services.ServiceInfo; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.List; + +import static java.util.Arrays.asList; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + + +@ExtendWith(MockitoExtension.class) +class GatewayScanJobTest { + + @Mock + private ServiceInstance instanceOne; + @Mock + private ServiceInstance instanceTwo; + @Mock + private List apimlServicesOne; + @Mock + private List apimlServicesTwo; + @Mock + private GatewayIndexService gatewayIndexerService; + @Mock + private InstanceInfoService instanceInfoService; + @Mock + private BasicInfoService basicInfoService; + @Mock + private EurekaRegistration serviceRegistration; + @InjectMocks + private GatewayScanJob gatewayScanJob; + + @BeforeEach + public void setUp() { + ReflectionTestUtils.setField(gatewayScanJob, "maxSimultaneousRequests", 3); + + lenient().when(instanceInfoService.getServiceInstance(CoreService.GATEWAY.getServiceId())).thenReturn(Mono.just(asList(instanceOne, instanceTwo))); + + lenient().when(gatewayIndexerService.indexGatewayServices(instanceOne)).thenReturn(Mono.just(apimlServicesOne)); + lenient().when(gatewayIndexerService.indexGatewayServices(instanceTwo)).thenReturn(Mono.just(apimlServicesTwo)); + } + + @Nested + class WhenScanningExternalGateway { + @Test + void shouldTriggerIndexingForRegisteredGateways() { + StepVerifier.create(gatewayScanJob.doScanExternalGateway()) + .expectNext(apimlServicesOne) + .expectNext(apimlServicesTwo) + .verifyComplete(); + + verify(gatewayIndexerService).indexGatewayServices(instanceOne); + verify(gatewayIndexerService).indexGatewayServices(instanceTwo); + verifyNoMoreInteractions(gatewayIndexerService); + } + } + + @Test + void scheduledCallShouldTriggerReactiveAction() { + GatewayScanJob spy = spy(gatewayScanJob); + when(spy.doScanExternalGateway()).thenReturn(Flux.empty()); + + spy.startScanExternalGatewayJob(); + + verify(spy).doScanExternalGateway(); + } +} diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/CertificateChainServiceTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/CertificateChainServiceTest.java index 11249b3948..0f116d764b 100644 --- a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/CertificateChainServiceTest.java +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/CertificateChainServiceTest.java @@ -11,6 +11,7 @@ package org.zowe.apiml.cloudgatewayservice.service; import lombok.NonNull; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -97,11 +98,15 @@ void setup() throws CertificateException { ReflectionTestUtils.setField(certificateChainService, "certificates", certificates, Certificate[].class); } + private String normalizeText(String input) { + return StringUtils.replace(input, "\r\n", "\n"); + } + @Test void whenGetCertificates_thenPEMIsProduced() { - String result = certificateChainService.getCertificatesInPEMFormat(); + String result = normalizeText(certificateChainService.getCertificatesInPEMFormat()); assertNotNull(result); - assertEquals(CERTIFICATE_1 + CERTIFICATE_2, result); + assertEquals(normalizeText(CERTIFICATE_1 + CERTIFICATE_2), result); } } diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/GatewayIndexServiceTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/GatewayIndexServiceTest.java new file mode 100644 index 0000000000..30c36c5cde --- /dev/null +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/GatewayIndexServiceTest.java @@ -0,0 +1,194 @@ +/* + * 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.service; + + +import io.netty.handler.ssl.SslContext; +import org.apache.groovy.util.Maps; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import org.springframework.web.reactive.function.client.WebClient; +import org.zowe.apiml.services.ServiceInfo; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static java.util.Arrays.asList; +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.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.zowe.apiml.cloudgatewayservice.service.WebClientHelperTest.KEYSTORE_PATH; +import static org.zowe.apiml.cloudgatewayservice.service.WebClientHelperTest.PASSWORD; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.APIML_ID; + +@ExtendWith(MockitoExtension.class) +class GatewayIndexServiceTest { + private GatewayIndexService gatewayIndexService; + private final ParameterizedTypeReference> serviceInfoType = new ParameterizedTypeReference>() { + }; + private ServiceInfo serviceInfoA, serviceInfoB; + private WebClient webClient; + private final String apiCatalogApiId = "zowe.apiml.apicatalog"; + @Mock + private ClientResponse clientResponse; + @Mock + private ExchangeFunction exchangeFunction; + @Mock + private ServiceInstance eurekaInstance; + + @BeforeEach + void setUp() { + + lenient().when(eurekaInstance.getMetadata()).thenReturn(Maps.of(APIML_ID, "testApimlIdA")); + lenient().when(eurekaInstance.getInstanceId()).thenReturn("testInstanceIdA"); + + serviceInfoA = new ServiceInfo(); + serviceInfoB = new ServiceInfo(); + + serviceInfoB.setApiml(new ServiceInfo.Apiml()); + ServiceInfo.ApiInfoExtended sysviewApiInfo = new ServiceInfo.ApiInfoExtended(); + sysviewApiInfo.setApiId(apiCatalogApiId); + + serviceInfoB.getApiml().setApiInfo(Collections.singletonList(sysviewApiInfo)); + + webClient = spy(WebClient.builder().exchangeFunction(exchangeFunction).build()); + gatewayIndexService = new GatewayIndexService(webClient, 60, null, null, null); + } + + @Nested + class WhenIndexingGatewayService { + + @BeforeEach + void setUp() { + lenient().when(exchangeFunction.exchange(any(ClientRequest.class))) + .thenReturn(Mono.just(clientResponse)); + + lenient().when(clientResponse.bodyToMono(serviceInfoType)).thenReturn(Mono.just(Arrays.asList(serviceInfoA, serviceInfoB))); + } + + @Test + void shouldCacheListOfTheServices() { + + StepVerifier.FirstStep> servicesVerifier = StepVerifier.create(gatewayIndexService.indexGatewayServices(eurekaInstance)); + + servicesVerifier + .expectNext(asList(serviceInfoA, serviceInfoB)) + .verifyComplete(); + + verify(exchangeFunction).exchange(any()); + + Map> allServices = gatewayIndexService.listRegistry(null, null); + + assertThat(allServices).containsOnlyKeys("testApimlIdA"); + assertThat(allServices.get("testApimlIdA")).containsExactlyInAnyOrder(serviceInfoA, serviceInfoB); + verifyNoMoreInteractions(exchangeFunction); + } + + @Test + void shouldGenerateSyntheticApimlIdCacheKey() { + when(eurekaInstance.getMetadata()).thenReturn(null); + + StepVerifier.FirstStep> servicesVerifier = StepVerifier.create(gatewayIndexService.indexGatewayServices(eurekaInstance)); + + servicesVerifier + .expectNext(asList(serviceInfoA, serviceInfoB)) + .verifyComplete(); + + Map> allServices = gatewayIndexService.listRegistry(null, null); + + assertThat(allServices).containsOnlyKeys("SUBSTITUTE_testInstanceIdA"); + } + + @Test + void shouldFilterCachedServicesByApiId() { + + StepVerifier.create(gatewayIndexService.indexGatewayServices(eurekaInstance)) + .expectNext(asList(serviceInfoA, serviceInfoB)) + .verifyComplete(); + + Map> allServices = gatewayIndexService.listRegistry(null, apiCatalogApiId); + + assertThat(allServices).containsOnly(new AbstractMap.SimpleEntry<>("testApimlIdA", Collections.singletonList(serviceInfoB))); + } + + @Test + void shouldReturnEmptyMapForNotExistingApimlId() { + assertThat(gatewayIndexService.listRegistry("unknownId", null)).isEmpty(); + assertThat(gatewayIndexService.listRegistry(null, "unknownApiId")).isEmpty(); + assertThat(gatewayIndexService.listRegistry("unknownId", "unknownApiId")).isEmpty(); + } + } + + @Nested + class WhenUsingCustomClientKey { + + @Test + void shouldInitializeCustomSslContext() { + + gatewayIndexService = new GatewayIndexService(webClient, 60, KEYSTORE_PATH, PASSWORD, "PKCS12"); + + SslContext customClientSslContext = (SslContext) ReflectionTestUtils.getField(gatewayIndexService, "customClientSslContext"); + + assertThat(customClientSslContext).isNotNull(); + } + + @Test + void shouldNotUseDefaultWebClientWhenCustomContextIdProvided() { + gatewayIndexService = new GatewayIndexService(webClient, 60, KEYSTORE_PATH, PASSWORD, "PKCS12"); + + StepVerifier.create(gatewayIndexService.indexGatewayServices(eurekaInstance)) + .verifyComplete(); + + verifyNoInteractions(webClient); + } + + @Test + void shouldSkipCustomSslContextCreationIfPasswordNotDefined() { + + gatewayIndexService = new GatewayIndexService(webClient, 60, KEYSTORE_PATH, null, null); + + SslContext customClientSslContext = (SslContext) ReflectionTestUtils.getField(gatewayIndexService, "customClientSslContext"); + + assertThat(customClientSslContext).isNull(); + } + + @Test + void shouldUseDefaultWebClientWhenCustomSslContextIsNotProvided() { + gatewayIndexService = new GatewayIndexService(webClient, 60, null, null, null); + + StepVerifier.create(gatewayIndexService.indexGatewayServices(eurekaInstance)) + .verifyComplete(); + + verify(webClient).mutate(); + } + + } +} \ No newline at end of file diff --git a/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/WebClientHelperTest.java b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/WebClientHelperTest.java new file mode 100644 index 0000000000..3e715b3506 --- /dev/null +++ b/cloud-gateway-service/src/test/java/org/zowe/apiml/cloudgatewayservice/service/WebClientHelperTest.java @@ -0,0 +1,48 @@ +/* + * 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.service; + +import io.netty.handler.ssl.SslContext; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.zowe.apiml.security.HttpsConfigError; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.AssertionsForClassTypes.catchThrowableOfType; +import static org.zowe.apiml.security.HttpsConfigError.ErrorCode.HTTP_CLIENT_INITIALIZATION_FAILED; + +class WebClientHelperTest { + private static final char[] WRONG_PASSWORD = "wrong_password".toCharArray(); // NOSONAR + static final String KEYSTORE_PATH = "../keystore/localhost/localhost.keystore.p12"; + static final char[] PASSWORD = "password".toCharArray(); // NOSONAR + + @Nested + class WhenLoading { + @Test + void givenWrongPath_thenThrowException() { + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> WebClientHelper.load("../wrong/path", PASSWORD, "PKCS12")); + } + + @Test + void givenCorrectPath_thenLoadSSLContext() { + final SslContext sslContext = WebClientHelper.load(KEYSTORE_PATH, PASSWORD, "PKCS12"); + assertThat(sslContext).isNotNull(); + } + + @Test + void givenWrongPassword_httpConfigErrorIsExpected() { + HttpsConfigError error = catchThrowableOfType(() -> WebClientHelper.load("../keystore/localhost/localhost.keystore.p12", WRONG_PASSWORD, "PKCS12"), HttpsConfigError.class); + assertThat(error.getCode()).isEqualTo(HTTP_CLIENT_INITIALIZATION_FAILED); + assertThat(error.getConfig()).isNull(); + } + } +} diff --git a/cloud-gateway-service/src/test/resources/application.yml b/cloud-gateway-service/src/test/resources/application.yml index f6811128b6..3d3e43eedc 100644 --- a/cloud-gateway-service/src/test/resources/application.yml +++ b/cloud-gateway-service/src/test/resources/application.yml @@ -9,9 +9,13 @@ apiml: id: cloud-gateway port: 10023 hostname: localhost + scheme: https 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 + cloudGateway: + serviceRegistryEnabled: false forwardClientCertEnabled: false + server: port: ${apiml.service.port} ssl: diff --git a/common-service-core/src/main/java/org/zowe/apiml/security/HttpsConfigError.java b/common-service-core/src/main/java/org/zowe/apiml/security/HttpsConfigError.java index 74a31c2345..ec5e316fde 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/security/HttpsConfigError.java +++ b/common-service-core/src/main/java/org/zowe/apiml/security/HttpsConfigError.java @@ -45,6 +45,12 @@ public HttpsConfigError(Throwable cause, ErrorCode code, HttpsConfig config) { this.config = config; } + public HttpsConfigError(String message, Throwable cause, ErrorCode code) { + super(message, cause); + this.code = code; + this.config = null; + } + public ErrorCode getCode() { return this.code; } diff --git a/common-service-core/src/main/java/org/zowe/apiml/services/BasicInfoService.java b/common-service-core/src/main/java/org/zowe/apiml/services/BasicInfoService.java new file mode 100644 index 0000000000..de39a5a5dd --- /dev/null +++ b/common-service-core/src/main/java/org/zowe/apiml/services/BasicInfoService.java @@ -0,0 +1,160 @@ +/* + * 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.services; + + +import com.fasterxml.jackson.core.Version; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.EurekaClient; +import com.netflix.discovery.shared.Application; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.ObjectUtils; +import org.zowe.apiml.auth.Authentication; +import org.zowe.apiml.config.ApiInfo; +import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; + +import java.util.AbstractMap; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.minBy; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.SERVICE_DESCRIPTION; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.SERVICE_TITLE; +import static org.zowe.apiml.services.ServiceInfo.ApiInfoExtended; +import static org.zowe.apiml.services.ServiceInfoUtils.getBasePath; +import static org.zowe.apiml.services.ServiceInfoUtils.getInstances; +import static org.zowe.apiml.services.ServiceInfoUtils.getMajorVersion; +import static org.zowe.apiml.services.ServiceInfoUtils.getStatus; +import static org.zowe.apiml.services.ServiceInfoUtils.getVersion; + +/** + * Similar to {@link org.zowe.apiml.gateway.services.ServicesInfoService} service which does not depend on gateway-service components. + * Following properties left blank: + * {@link ServiceInfo.Service#homePageUrl} and {@link ServiceInfo.ApiInfoExtended#swaggerUrl} + */ +@Slf4j +@RequiredArgsConstructor +public class BasicInfoService { + + private final EurekaClient eurekaClient; + private final EurekaMetadataParser eurekaMetadataParser; + + public List getServicesInfo() { + return eurekaClient.getApplications().getRegisteredApplications() + .stream().map(this::getServiceInfo) + .collect(Collectors.toCollection(LinkedList::new)); + } + + private ServiceInfo getServiceInfo(Application application) { + String serviceId = application.getName().toLowerCase(); + + List appInstances = application.getInstances(); + if (ObjectUtils.isEmpty(appInstances)) { + return ServiceInfo.builder() + .serviceId(serviceId) + .status(InstanceInfo.InstanceStatus.DOWN) + .build(); + } + + return ServiceInfo.builder() + .serviceId(serviceId) + .status(getStatus(appInstances)) + .apiml(getApiml(appInstances)) + .instances(getInstances(appInstances)) + .build(); + } + + /** + * uses simplified: + * - getApiInfos + * - getApiInfos + */ + private ServiceInfo.Apiml getApiml(List appInstances) { + return ServiceInfo.Apiml.builder() + .apiInfo(getApiInfos(appInstances)) + .service(getService(appInstances)) + .authentication(getAuthentication(appInstances)) + .build(); + } + + + /** + * simplified version, following part is excluded: + * - homePageUrl + */ + private ServiceInfo.Service getService(List appInstances) { + InstanceInfo instanceInfo = getInstanceWithHighestVersion(appInstances); + + return ServiceInfo.Service.builder() + .title(instanceInfo.getMetadata().get(SERVICE_TITLE)) + .description(instanceInfo.getMetadata().get(SERVICE_DESCRIPTION)) + .build(); + } + + /** + * Simplified version, following properties are excluded: + * - baseUrl + * - swaggerUrl + */ + private List getApiInfos(List appInstances) { + return appInstances.stream() + .map(instanceInfo -> new AbstractMap.SimpleEntry<>(instanceInfo, eurekaMetadataParser.parseApiInfo(instanceInfo.getMetadata()))) + .flatMap(entry -> entry.getValue().stream() + .map(apiInfo -> ApiInfoExtended.builder() + .apiId(apiInfo.getApiId()) + .basePath(getBasePath(apiInfo, entry.getKey())) + .gatewayUrl(apiInfo.getGatewayUrl()) + .documentationUrl(apiInfo.getDocumentationUrl()) + .version(apiInfo.getVersion()) + .codeSnippet(apiInfo.getCodeSnippet()) + .isDefaultApi(apiInfo.isDefaultApi()) + .build())) + .collect(groupingBy( + apiInfo -> new AbstractMap.SimpleEntry<>(apiInfo.getApiId(), getMajorVersion(apiInfo)), + minBy(Comparator.comparingInt(ServiceInfoUtils::getMajorVersion)) + )) + .values() + .stream() + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } + + private List getAuthentication(List appInstances) { + return appInstances.stream() + .map(instanceInfo -> eurekaMetadataParser.parseAuthentication(instanceInfo.getMetadata())) + .filter(a -> !a.isEmpty()) + .distinct() + .collect(Collectors.toList()); + } + + private InstanceInfo getInstanceWithHighestVersion(List appInstances) { + InstanceInfo instanceInfo = appInstances.get(0); + Version highestVersion = Version.unknownVersion(); + + for (InstanceInfo currentInfo : appInstances) { + List apiInfoList = eurekaMetadataParser.parseApiInfo(currentInfo.getMetadata()); + for (ApiInfo apiInfo : apiInfoList) { + Version version = getVersion(apiInfo.getVersion()); + if (version.compareTo(highestVersion) > 0) { + highestVersion = version; + instanceInfo = currentInfo; + } + } + } + return instanceInfo; + } +} diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/services/ServiceInfo.java b/common-service-core/src/main/java/org/zowe/apiml/services/ServiceInfo.java similarity index 86% rename from apiml-common/src/main/java/org/zowe/apiml/product/services/ServiceInfo.java rename to common-service-core/src/main/java/org/zowe/apiml/services/ServiceInfo.java index c91d606d27..954676401a 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/services/ServiceInfo.java +++ b/common-service-core/src/main/java/org/zowe/apiml/services/ServiceInfo.java @@ -8,13 +8,15 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.product.services; +package org.zowe.apiml.services; import com.fasterxml.jackson.annotation.JsonInclude; import com.netflix.appinfo.InstanceInfo; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import org.zowe.apiml.auth.Authentication; import org.zowe.apiml.config.ApiInfo; @@ -24,6 +26,8 @@ @Data @Builder +@NoArgsConstructor +@AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class ServiceInfo { private String serviceId; @@ -33,6 +37,8 @@ public class ServiceInfo { @Data @Builder + @NoArgsConstructor + @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public static class Apiml { private List apiInfo; @@ -42,6 +48,8 @@ public static class Apiml { @Data @Builder + @NoArgsConstructor + @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public static class Instances { private InstanceInfo.InstanceStatus status; @@ -57,6 +65,8 @@ public static class Instances { @Data @Builder + @NoArgsConstructor + @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public static class Service { private String title; @@ -67,6 +77,8 @@ public static class Service { @Data @SuperBuilder @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public static class ApiInfoExtended extends ApiInfo { private String baseUrl; diff --git a/common-service-core/src/main/java/org/zowe/apiml/services/ServiceInfoUtils.java b/common-service-core/src/main/java/org/zowe/apiml/services/ServiceInfoUtils.java new file mode 100644 index 0000000000..24840f8dae --- /dev/null +++ b/common-service-core/src/main/java/org/zowe/apiml/services/ServiceInfoUtils.java @@ -0,0 +1,105 @@ +/* + * 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.services; + +import com.fasterxml.jackson.core.Version; +import com.netflix.appinfo.InstanceInfo; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.zowe.apiml.config.ApiInfo; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Utility class containing mapping functions for ServiceInfo formatting + */ +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ServiceInfoUtils { + + public static Map getInstances(List appInstances) { + return appInstances.stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap( + InstanceInfo::getInstanceId, + instanceInfo -> ServiceInfo.Instances.builder() + .status(instanceInfo.getStatus()) + .hostname(instanceInfo.getHostName()) + .ipAddr(instanceInfo.getIPAddr()) + .protocol(getProtocol(instanceInfo)) + .port(getPort(instanceInfo)) + .homePageUrl(instanceInfo.getHomePageUrl()) + .healthCheckUrl(getHealthCheckUrl(instanceInfo)) + .statusPageUrl(instanceInfo.getStatusPageUrl()) + .customMetadata(getCustomMetadata(instanceInfo.getMetadata())) + .build() + )); + } + + public static String getBasePath(ApiInfo apiInfo, InstanceInfo instanceInfo) { + return String.format("/%s/%s", instanceInfo.getAppName().toLowerCase(), apiInfo.getGatewayUrl()); + } + + private static String getHealthCheckUrl(InstanceInfo instanceInfo) { + return instanceInfo.isPortEnabled(InstanceInfo.PortType.SECURE) ? + instanceInfo.getSecureHealthCheckUrl() : instanceInfo.getHealthCheckUrl(); + } + + private static int getPort(InstanceInfo instanceInfo) { + return instanceInfo.isPortEnabled(InstanceInfo.PortType.SECURE) ? + instanceInfo.getSecurePort() : instanceInfo.getPort(); + } + + private static String getProtocol(InstanceInfo instanceInfo) { + return instanceInfo.isPortEnabled(InstanceInfo.PortType.SECURE) ? "https" : "http"; + } + + public static int getMajorVersion(ServiceInfo.ApiInfoExtended apiInfo) { + return getVersion(apiInfo.getVersion()).getMajorVersion(); + } + + public static Map getCustomMetadata(Map metadata) { + return metadata.entrySet().stream() + .filter(entry -> !entry.getKey().startsWith("apiml.")) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + public static Version getVersion(String version) { + if (version == null) return Version.unknownVersion(); + + String[] versions = version.split("\\."); + + int major = 0; + int minor = 0; + int patch = 0; + try { + if (versions.length >= 1) major = Integer.parseInt(versions[0]); + if (versions.length >= 2) minor = Integer.parseInt(versions[1]); + if (versions.length >= 3) patch = Integer.parseInt(versions[2]); + } catch (NumberFormatException ex) { + log.debug("Incorrect version {}", version); + } + + return new Version(major, minor, patch, null, null, null); + } + + public static InstanceInfo.InstanceStatus getStatus(List instances) { + if (instances.stream().anyMatch(instance -> instance.getStatus().equals(InstanceInfo.InstanceStatus.UP))) { + return InstanceInfo.InstanceStatus.UP; + } else { + return InstanceInfo.InstanceStatus.DOWN; + } + } +} diff --git a/common-service-core/src/test/java/org/zowe/apiml/services/BasicInfoServiceTest.java b/common-service-core/src/test/java/org/zowe/apiml/services/BasicInfoServiceTest.java new file mode 100644 index 0000000000..9fb1b81907 --- /dev/null +++ b/common-service-core/src/test/java/org/zowe/apiml/services/BasicInfoServiceTest.java @@ -0,0 +1,139 @@ +/* + * 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.services; + + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.EurekaClient; +import com.netflix.discovery.shared.Application; +import com.netflix.discovery.shared.Applications; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.zowe.apiml.auth.AuthenticationScheme; +import org.zowe.apiml.config.ApiInfo; +import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasProperty; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES_GATEWAY_URL; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES_SERVICE_URL; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.SERVICE_DESCRIPTION; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.SERVICE_TITLE; + +@ExtendWith(MockitoExtension.class) +class BasicInfoServiceTest { + + // Client test configuration + private static final String CLIENT_SERVICE_ID = "testclient"; + private static final String CLIENT_INSTANCE_ID = CLIENT_SERVICE_ID + ":"; + private static final String CLIENT_HOSTNAME = "client"; + private static final String CLIENT_IP = "192.168.0.1"; + private static final int CLIENT_PORT = 10; + private static final String CLIENT_HOMEPAGE = "https://client:10"; + private static final String CLIENT_RELATIVE_HEALTH_URL = "/actuator/health"; + private static final String CLIENT_STATUS_URL = "https://client:10/actuator/info"; + private static final String CLIENT_API_ID = "zowe.client.api"; + private static final String CLIENT_API_VERSION = "1.0.0"; + private static final String CLIENT_API_GW_URL = "api/v1"; + private static final boolean CLIENT_API_DEFAULT = true; + private static final String CLIENT_API_SWAGGER_URL = CLIENT_HOMEPAGE + "/apiDoc"; + private static final String CLIENT_API_DOC_URL = "https://www.zowe.org"; + private static final String CLIENT_ROUTE_UI = "ui/v1"; + private static final String CLIENT_SERVICE_TITLE = "Client service"; + private static final String CLIENT_SERVICE_DESCRIPTION = "Client test service"; + private static final AuthenticationScheme CLIENT_AUTHENTICATION_SCHEME = AuthenticationScheme.ZOSMF; + private static final String CLIENT_AUTHENTICATION_APPLID = "authid"; + private static final String CLIENT_CUSTOM_METADATA_KEY = "custom.test.key"; + private static final String CLIENT_CUSTOM_METADATA_VALUE = "value"; + + // ServiceInfo properties + private static final String SERVICE_SERVICE_ID = "serviceId"; + + @Mock + private EurekaClient eurekaClient; + + private final EurekaMetadataParser eurekaMetadataParser = new EurekaMetadataParser(); + + private BasicInfoService basicInfoService; + + @BeforeEach + void setUp() { + basicInfoService = new BasicInfoService(eurekaClient, eurekaMetadataParser); + } + + @Test + void whenListingAllServices_thenReturnList() { + String clientServiceId2 = "testclient2"; + + List applications = Arrays.asList( + new Application(CLIENT_SERVICE_ID, Collections.singletonList(createFullTestInstance())), + new Application(clientServiceId2) + ); + when(eurekaClient.getApplications()) + .thenReturn(new Applications(null, 1L, applications)); + + List servicesInfo = basicInfoService.getServicesInfo(); + + + assertEquals(2, servicesInfo.size()); + assertThat(servicesInfo, contains( + hasProperty(SERVICE_SERVICE_ID, is(CLIENT_SERVICE_ID)), + hasProperty(SERVICE_SERVICE_ID, is(clientServiceId2)) + )); + } + + private InstanceInfo createFullTestInstance() { + ApiInfo apiInfo = ApiInfo.builder() + .apiId(CLIENT_API_ID) + .version(CLIENT_API_VERSION) + .gatewayUrl(CLIENT_API_GW_URL) + .isDefaultApi(CLIENT_API_DEFAULT) + .swaggerUrl(CLIENT_API_SWAGGER_URL) + .documentationUrl(CLIENT_API_DOC_URL) + .build(); + Map metadata = EurekaMetadataParser.generateMetadata(CLIENT_SERVICE_ID, apiInfo); + metadata.put(SERVICE_TITLE, CLIENT_SERVICE_TITLE); + metadata.put(SERVICE_DESCRIPTION, CLIENT_SERVICE_DESCRIPTION); + metadata.put(ROUTES + ".ui-v1." + ROUTES_SERVICE_URL, "/"); + metadata.put(ROUTES + ".ui-v1." + ROUTES_GATEWAY_URL, CLIENT_ROUTE_UI); + metadata.put(AUTHENTICATION_SCHEME, CLIENT_AUTHENTICATION_SCHEME.getScheme()); + metadata.put(AUTHENTICATION_APPLID, CLIENT_AUTHENTICATION_APPLID); + metadata.put(CLIENT_CUSTOM_METADATA_KEY, CLIENT_CUSTOM_METADATA_VALUE); + + return InstanceInfo.Builder.newBuilder() + .setAppName(CLIENT_SERVICE_ID) + .setInstanceId(CLIENT_INSTANCE_ID + Math.random()) + .setHostName(CLIENT_HOSTNAME) + .setIPAddr(CLIENT_IP) + .enablePort(InstanceInfo.PortType.SECURE, true) + .setSecurePort(CLIENT_PORT) + .setHomePageUrl(null, CLIENT_HOMEPAGE) + .setHealthCheckUrls(CLIENT_RELATIVE_HEALTH_URL, null, null) + .setStatusPageUrl(null, CLIENT_STATUS_URL) + .setMetadata(metadata) + .build(); + } +} \ No newline at end of file diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServerInfoConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServerInfoConfig.java index de0ee3f94f..144e88fba6 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServerInfoConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServerInfoConfig.java @@ -16,7 +16,6 @@ import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser; import org.zowe.apiml.product.gateway.GatewayConfigProperties; import org.zowe.apiml.product.routing.transform.TransformService; -import org.zowe.apiml.product.services.ServicesInfoService; @Configuration public class ServerInfoConfig { @@ -26,7 +25,6 @@ public EurekaMetadataParser getEurekaMetadataParser() { return new EurekaMetadataParser(); } - @Bean public ServicesInfoService servicesInfoService(EurekaClient eurekaClient, EurekaMetadataParser eurekaMetadataParser, GatewayConfigProperties gatewayConfigProperties, TransformService transformService) { diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoController.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoController.java index e81da26350..513a5df9f8 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoController.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoController.java @@ -22,13 +22,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.zowe.apiml.product.services.ServiceInfo; -import org.zowe.apiml.product.services.ServicesInfoService; +import org.zowe.apiml.services.ServiceInfo; import java.util.List; -import static org.zowe.apiml.product.services.ServicesInfoService.CURRENT_VERSION; -import static org.zowe.apiml.product.services.ServicesInfoService.VERSION_HEADER; +import static org.zowe.apiml.gateway.services.ServicesInfoService.CURRENT_VERSION; +import static org.zowe.apiml.gateway.services.ServicesInfoService.VERSION_HEADER; @RestController diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/services/ServicesInfoService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoService.java similarity index 74% rename from apiml-common/src/main/java/org/zowe/apiml/product/services/ServicesInfoService.java rename to gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoService.java index 45280f5491..b40d5c9c68 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/services/ServicesInfoService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/services/ServicesInfoService.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.product.services; +package org.zowe.apiml.gateway.services; import com.fasterxml.jackson.core.Version; import com.netflix.appinfo.InstanceInfo; @@ -26,13 +26,14 @@ import org.zowe.apiml.product.routing.ServiceType; import org.zowe.apiml.product.routing.transform.TransformService; import org.zowe.apiml.product.routing.transform.URLTransformationException; +import org.zowe.apiml.services.ServiceInfo; +import org.zowe.apiml.services.ServiceInfoUtils; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -41,6 +42,10 @@ import static java.util.stream.Collectors.minBy; import static org.zowe.apiml.constants.EurekaMetadataDefinition.SERVICE_DESCRIPTION; import static org.zowe.apiml.constants.EurekaMetadataDefinition.SERVICE_TITLE; +import static org.zowe.apiml.services.ServiceInfoUtils.getBasePath; +import static org.zowe.apiml.services.ServiceInfoUtils.getInstances; +import static org.zowe.apiml.services.ServiceInfoUtils.getMajorVersion; +import static org.zowe.apiml.services.ServiceInfoUtils.getVersion; @Slf4j @RequiredArgsConstructor @@ -90,6 +95,11 @@ public ServiceInfo getServiceInfo(String serviceId) { return getServiceInfo(application); } + private String getBaseUrl(ApiInfo apiInfo, InstanceInfo instanceInfo) { + return String.format("%s://%s%s", + gatewayConfigProperties.getScheme(), gatewayConfigProperties.getHostname(), getBasePath(apiInfo, instanceInfo)); + } + private ServiceInfo getServiceInfo(Application application) { String serviceId = application.getName().toLowerCase(); @@ -145,7 +155,7 @@ private List getApiInfos(List appInst return completeList.stream() .collect(groupingBy( apiInfo -> new AbstractMap.SimpleEntry<>(apiInfo.getApiId(), getMajorVersion(apiInfo)), - minBy(Comparator.comparingInt(this::getMajorVersion)) + minBy(Comparator.comparingInt(ServiceInfoUtils::getMajorVersion)) )) .values() .stream() @@ -175,24 +185,6 @@ private List getAuthentication(List appInstances) .collect(Collectors.toList()); } - private Map getInstances(List appInstances) { - return appInstances.stream() - .collect(Collectors.toMap( - InstanceInfo::getInstanceId, - instanceInfo -> ServiceInfo.Instances.builder() - .status(instanceInfo.getStatus()) - .hostname(instanceInfo.getHostName()) - .ipAddr(instanceInfo.getIPAddr()) - .protocol(getProtocol(instanceInfo)) - .port(getPort(instanceInfo)) - .homePageUrl(instanceInfo.getHomePageUrl()) - .healthCheckUrl(getHealthCheckUrl(instanceInfo)) - .statusPageUrl(instanceInfo.getStatusPageUrl()) - .customMetadata(getCustomMetadata(instanceInfo.getMetadata())) - .build() - )); - } - private String getGatewayUrl(String url, String serviceId, ServiceType type, RoutedServices routes) { if (url == null) return null; @@ -207,25 +199,6 @@ private String getGatewayUrl(String url, String serviceId, ServiceType type, Rou } } - private Version getVersion(String version) { - if (version == null) return Version.unknownVersion(); - - String[] versions = version.split("\\."); - - int major = 0; - int minor = 0; - int patch = 0; - try { - if (versions.length >= 1) major = Integer.parseInt(versions[0]); - if (versions.length >= 2) minor = Integer.parseInt(versions[1]); - if (versions.length >= 3) patch = Integer.parseInt(versions[2]); - } catch (NumberFormatException ex) { - log.debug("Incorrect version {}", version); - } - - return new Version(major, minor, patch, null, null, null); - } - private InstanceInfo.InstanceStatus getStatus(List instances) { if (instances.stream().anyMatch(instance -> instance.getStatus().equals(InstanceInfo.InstanceStatus.UP))) { return InstanceInfo.InstanceStatus.UP; @@ -234,39 +207,6 @@ private InstanceInfo.InstanceStatus getStatus(List instances) { } } - private Map getCustomMetadata(Map metadata) { - return metadata.entrySet().stream() - .filter(entry -> !entry.getKey().startsWith("apiml.")) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } - - private String getBasePath(ApiInfo apiInfo, InstanceInfo instanceInfo) { - return String.format("/%s/%s", instanceInfo.getAppName().toLowerCase(), apiInfo.getGatewayUrl()); - } - - private String getBaseUrl(ApiInfo apiInfo, InstanceInfo instanceInfo) { - return String.format("%s://%s%s", - gatewayConfigProperties.getScheme(), gatewayConfigProperties.getHostname(), getBasePath(apiInfo, instanceInfo)); - } - - private String getHealthCheckUrl(InstanceInfo instanceInfo) { - return instanceInfo.isPortEnabled(InstanceInfo.PortType.SECURE) ? - instanceInfo.getSecureHealthCheckUrl() : instanceInfo.getHealthCheckUrl(); - } - - private int getPort(InstanceInfo instanceInfo) { - return instanceInfo.isPortEnabled(InstanceInfo.PortType.SECURE) ? - instanceInfo.getSecurePort() : instanceInfo.getPort(); - } - - private String getProtocol(InstanceInfo instanceInfo) { - return instanceInfo.isPortEnabled(InstanceInfo.PortType.SECURE) ? "https" : "http"; - } - - private int getMajorVersion(ServiceInfo.ApiInfoExtended apiInfo) { - return getVersion(apiInfo.getVersion()).getMajorVersion(); - } - private InstanceInfo getInstanceWithHighestVersion(List appInstances) { InstanceInfo instanceInfo = appInstances.get(0); Version highestVersion = Version.unknownVersion(); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/services/ServicesInfoControllerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/services/ServicesInfoControllerTest.java index 9b74508175..e2da7ce804 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/services/ServicesInfoControllerTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/services/ServicesInfoControllerTest.java @@ -16,8 +16,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; -import org.zowe.apiml.product.services.ServiceInfo; -import org.zowe.apiml.product.services.ServicesInfoService; +import org.zowe.apiml.services.ServiceInfo; import java.util.Arrays; import java.util.Collections; @@ -26,8 +25,8 @@ import static org.hamcrest.core.Is.is; import static org.mockito.Mockito.when; import static org.zowe.apiml.gateway.services.ServicesInfoController.SERVICES_URL; -import static org.zowe.apiml.product.services.ServicesInfoService.CURRENT_VERSION; -import static org.zowe.apiml.product.services.ServicesInfoService.VERSION_HEADER; +import static org.zowe.apiml.gateway.services.ServicesInfoService.CURRENT_VERSION; +import static org.zowe.apiml.gateway.services.ServicesInfoService.VERSION_HEADER; @ExtendWith(MockitoExtension.class) class ServicesInfoControllerTest { diff --git a/apiml-common/src/test/java/org/zowe/apiml/product/services/ServicesInfoServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/services/ServicesInfoServiceTest.java similarity index 99% rename from apiml-common/src/test/java/org/zowe/apiml/product/services/ServicesInfoServiceTest.java rename to gateway-service/src/test/java/org/zowe/apiml/gateway/services/ServicesInfoServiceTest.java index 524cf037b8..5ef11637ce 100644 --- a/apiml-common/src/test/java/org/zowe/apiml/product/services/ServicesInfoServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/services/ServicesInfoServiceTest.java @@ -8,7 +8,8 @@ * Copyright Contributors to the Zowe Project. */ -package org.zowe.apiml.product.services; +package org.zowe.apiml.gateway.services; + import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; @@ -27,6 +28,7 @@ import org.zowe.apiml.product.gateway.GatewayClient; import org.zowe.apiml.product.gateway.GatewayConfigProperties; import org.zowe.apiml.product.routing.transform.TransformService; +import org.zowe.apiml.services.ServiceInfo; import java.util.Arrays; import java.util.Collections; diff --git a/integration-tests/src/test/java/org/zowe/apiml/startup/impl/ApiMediationLayerStartupChecker.java b/integration-tests/src/test/java/org/zowe/apiml/startup/impl/ApiMediationLayerStartupChecker.java index e18988e500..182f0af78e 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/startup/impl/ApiMediationLayerStartupChecker.java +++ b/integration-tests/src/test/java/org/zowe/apiml/startup/impl/ApiMediationLayerStartupChecker.java @@ -95,7 +95,7 @@ private boolean areAllServicesUp() { } String allComponents = context.read("$.components.discoveryComposite.components.discoveryClient.details.services").toString(); boolean isTestApplicationUp = allComponents.contains("discoverableclient"); - boolean isCloudGatewayUp = allComponents.contains("cloudgateway"); + boolean isCloudGatewayUp = allComponents.contains("cloud-gateway"); log.debug("Discoverable Client is {}", isTestApplicationUp); log.debug("Cloud gateway is {}", isCloudGatewayUp); diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/service/FullApiMediationLayer.java b/integration-tests/src/test/java/org/zowe/apiml/util/service/FullApiMediationLayer.java index 5933745d16..5973b367aa 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/service/FullApiMediationLayer.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/service/FullApiMediationLayer.java @@ -65,8 +65,8 @@ private void prepareNodeJsSampleApp() { // If NODE_HOME is defined in environment variable, use it, otherwise assume in PATH String path = Optional.ofNullable(System.getenv("NODE_HOME")) - .map(javaHome -> javaHome + "/bin/") - .orElse(""); + .map(javaHome -> javaHome + "/bin/") + .orElse(""); parameters.add(path + "node"); parameters.add("src/index.js"); @@ -92,7 +92,7 @@ public void prepareCaching() { } public void prepareCloudGateway() { - cloudGatewayService = new RunningService("cloudgateway","cloud-gateway-service/build/libs", null,null); + cloudGatewayService = new RunningService("cloud-gateway", "cloud-gateway-service/build/libs", null, null); } private void prepareMockServices() { @@ -134,9 +134,9 @@ public void start() { Map cachingEnv = new HashMap<>(env); cachingEnv.put("ZWE_configs_port", "10016"); cachingService.startWithScript("caching-service-package/src/main/resources/bin", cachingEnv); - Map cloudGWEnv = new HashMap<>(env); - cloudGWEnv.put("ZWE_configs_port","10023"); - cloudGatewayService.startWithScript("cloud-gateway-package/src/main/resources/bin",cloudGWEnv); + Map cloudGWEnv = new HashMap<>(env); + cloudGWEnv.put("ZWE_configs_port", "10023"); + cloudGatewayService.startWithScript("cloud-gateway-package/src/main/resources/bin", cloudGWEnv); if (!attlsEnabled) { nodeJsSampleApp = nodeJsBuilder.start(); }