Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH2883 services registry #3062

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cloud-gateway-package/src/main/resources/bin/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${CLOUD_GATEWAY_CODE} java \
-Dapiml.logs.location=${ZWE_zowe_logDirectory} \
-Dapiml.zoweManifest=${ZWE_zowe_runtimeDirectory}/manifest.json \
-Dserver.address=0.0.0.0 \
-Dapiml.service.apimlId=${ZWE_configs_apimlId:-} \
-Deureka.client.serviceUrl.defaultZone=${ZWE_DISCOVERY_SERVICES_LIST:-"https://${ZWE_haInstance_hostname:-localhost}:${ZWE_components_discovery_port:-7553}/eureka/"} \
-Dserver.ssl.enabled=${ZWE_configs_server_ssl_enabled:-true} \
-Dserver.maxConnectionsPerRoute=${ZWE_configs_server_maxConnectionsPerRoute:-100} \
Expand Down
4 changes: 3 additions & 1 deletion cloud-gateway-service/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -89,7 +91,7 @@ dependencies {
testAnnotationProcessor libs.lombok
testImplementation libs.spring.boot.starter.test
testImplementation libs.rest.assured

testImplementation libs.reactorTest
testImplementation libs.mockito.inline
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.cloudgatewayservice.service.BasicInfoService;
import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser;

@Configuration
public class RegistryConfig {

@Bean
public EurekaMetadataParser getEurekaMetadataParser() {
return new EurekaMetadataParser();
}

@Bean
public BasicInfoService basicInfoService(EurekaClient eurekaClient, EurekaMetadataParser eurekaMetadataParser) {
return new BasicInfoService(eurekaClient, eurekaMetadataParser);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.zowe.apiml.cloudgatewayservice.service.BasicInfoService;
import org.zowe.apiml.cloudgatewayservice.service.GatewayIndexService;
import org.zowe.apiml.cloudgatewayservice.service.InstanceInfoService;
import org.zowe.apiml.cloudgatewayservice.service.ServiceInfo;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.Map;
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:
* <pre>
* 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
* </pre>
*/
@EnableScheduling
@Slf4j
@Component
@ConditionalOnExpression("${apiml.cloudGateway.serviceRegistryEnabled:false}")
@RequiredArgsConstructor
public class GatewayScanJob {
public static final String GATEWAY_SERVICE_ID = "GATEWAY";

private final GatewayIndexService gatewayIndexerService;
private final InstanceInfoService instanceInfoService;
private final BasicInfoService basicInfoService;

@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(APIML_ID);
List<ServiceInfo> localServices = basicInfoService.getServicesInfo();
gatewayIndexerService.putApimlServices(apimlIdKey, localServices);
}

/**
* reactive entry point for the external gateways index refresh
*/
protected Flux<List<ServiceInfo>> doScanExternalGateway() {

Mono<List<ServiceInstance>> registeredGateways = instanceInfoService.getServiceInstance(GATEWAY_SERVICE_ID)
.map(gateways -> gateways.stream().filter(info -> !StringUtils.equals(info.getMetadata().getOrDefault(APIML_ID, "N/A"), currentApimlId)).collect(Collectors.toList()));

Flux<ServiceInstance> serviceInstanceFlux = registeredGateways.flatMapMany(Flux::fromIterable);

return serviceInstanceFlux
.flatMap(gatewayIndexerService::indexGatewayServices, maxSimultaneousRequests);
}

/**
* Helper to echo state os the services caches. Will be removed later when REST api
*/
@Scheduled(initialDelay = 10000, fixedDelayString = "5000")
void listCaches() {

Map<String, List<ServiceInfo>> fullState = gatewayIndexerService.listRegistry(null, null);

log.debug("Cache having {} apimlId records, {}", fullState.keySet().size(), System.currentTimeMillis());
for (String apimlId : fullState.keySet()) {
List<ServiceInfo> servicesInfo = gatewayIndexerService.listRegistry(apimlId, null).get(apimlId);
log.debug("\t {}-{} : found {} external services", apimlId, apimlId, servicesInfo.size());
}

Map<String, List<ServiceInfo>> allSysviews = gatewayIndexerService.listRegistry(null, "bcm.sysview");
for (Map.Entry<String, List<ServiceInfo>> apimlEntiry : allSysviews.entrySet()) {
log.debug("Listing all sysview services at: {}", apimlEntiry.getKey());
apimlEntiry.getValue().forEach(serviceInfo -> log.debug("\t {} - {}", serviceInfo.getServiceId(), serviceInfo.getInstances()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* 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.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.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
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.cloudgatewayservice.service.ServiceInfoUtils.getBasePath;
import static org.zowe.apiml.cloudgatewayservice.service.ServiceInfoUtils.getInstances;
import static org.zowe.apiml.cloudgatewayservice.service.ServiceInfoUtils.getMajorVersion;
import static org.zowe.apiml.cloudgatewayservice.service.ServiceInfoUtils.getStatus;
import static org.zowe.apiml.cloudgatewayservice.service.ServiceInfoUtils.getVersion;
import static org.zowe.apiml.constants.EurekaMetadataDefinition.SERVICE_DESCRIPTION;
import static org.zowe.apiml.constants.EurekaMetadataDefinition.SERVICE_TITLE;

/**
* 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<ServiceInfo> getServicesInfo() {
List<ServiceInfo> servicesInfo = new LinkedList<>();
for (Application application : eurekaClient.getApplications().getRegisteredApplications()) {
servicesInfo.add(getServiceInfo(application));
}

return servicesInfo;
}

private ServiceInfo getServiceInfo(Application application) {
String serviceId = application.getName().toLowerCase();

List<InstanceInfo> 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<InstanceInfo> 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<InstanceInfo> 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<ServiceInfo.ApiInfoExtended> getApiInfos(List<InstanceInfo> appInstances) {
List<ServiceInfo.ApiInfoExtended> completeList = new ArrayList<>();

for (InstanceInfo instanceInfo : appInstances) {
List<ApiInfo> apiInfoList = eurekaMetadataParser.parseApiInfo(instanceInfo.getMetadata());
completeList.addAll(apiInfoList.stream()
.map(apiInfo -> ServiceInfo.ApiInfoExtended.builder()
.apiId(apiInfo.getApiId())
.basePath(getBasePath(apiInfo, instanceInfo))
.gatewayUrl(apiInfo.getGatewayUrl())
.documentationUrl(apiInfo.getDocumentationUrl())
.version(apiInfo.getVersion())
.codeSnippet(apiInfo.getCodeSnippet())
.isDefaultApi(apiInfo.isDefaultApi())
.build())
.collect(Collectors.toList()));
}

return completeList.stream()
.collect(groupingBy(
apiInfo -> new AbstractMap.SimpleEntry<>(apiInfo.getApiId(), getMajorVersion(apiInfo)),
minBy(Comparator.comparingInt(ServiceInfoUtils::getMajorVersion))
))
.values()
.stream()
.map(Optional::get)
.collect(Collectors.toList());
}

private List<Authentication> getAuthentication(List<InstanceInfo> appInstances) {
return appInstances.stream()
.map(instanceInfo -> {
Authentication authentication = eurekaMetadataParser.parseAuthentication(instanceInfo.getMetadata());
return authentication.isEmpty() ? null : authentication;
})
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
}

private InstanceInfo getInstanceWithHighestVersion(List<InstanceInfo> appInstances) {
InstanceInfo instanceInfo = appInstances.get(0);
Version highestVersion = Version.unknownVersion();

for (InstanceInfo currentInfo : appInstances) {
List<ApiInfo> 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;
}
}
Loading
Loading