Skip to content

Commit

Permalink
Merge branch 'v3.x.x' into reboot/feat/validate-oidc-zaasclient
Browse files Browse the repository at this point in the history
  • Loading branch information
pablocarle authored Nov 12, 2024
2 parents d1cf7b0 + a94029b commit 1b6a635
Show file tree
Hide file tree
Showing 17 changed files with 826 additions and 374 deletions.
44 changes: 38 additions & 6 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,8 @@ jobs:
APIML_SERVICE_PORT: 10037
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: /
ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_GATEWAYURL: /
ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_SERVICEURL: /
zaas-service-2:
image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }}
env:
Expand All @@ -325,8 +325,8 @@ jobs:
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: /
ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_GATEWAYURL: /
ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_SERVICEURL: /

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -1098,7 +1098,7 @@ jobs:
- uses: ./.github/actions/teardown

CITestsDicoverableClientChaoticHA:
CITestsDiscoverableClientChaoticHA:
needs: PublishJibContainers
container: ubuntu:latest
runs-on: ubuntu-latest
Expand Down Expand Up @@ -1169,7 +1169,7 @@ jobs:
uses: actions/upload-artifact@v4
if: always()
with:
name: CITestsDicoverableClientChaoticHA-${{ env.JOB_ID }}
name: CITestsDiscoverableClientChaoticHA-${{ env.JOB_ID }}
path: |
integration-tests/build/reports/**
Expand Down Expand Up @@ -1366,11 +1366,40 @@ jobs:
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OKTA_USERNAMEATTRIBUTE: sub
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OKTA_JWKSETURI: ${{ secrets.OKTA_JWKSET_URI }}
APIML_SECURITY_OIDC_COOKIE_SAMESITE: None
APIML_HEALTH_PROTECTED: false
zaas-service:
image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }}
env:
APIML_HEALTH_PROTECTED: false
mock-services:
image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }}

discovery-service-2:
image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }}
volumes:
- /api-defs:/api-defs
env:
APIML_SERVICE_HOSTNAME: discovery-service-2
gateway-service-2:
image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }}
env:
APIML_SERVICE_APIMLID: apiml2
APIML_SERVICE_HOSTNAME: gateway-service-2
APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10011/eureka/
APIML_HEALTH_PROTECTED: false
ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_DISCOVERYSERVICEURLS: https://discovery-service:10011/eureka
ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_GATEWAYURL: /
ZWE_CONFIGS_APIML_SERVICE_ADDITIONALREGISTRATION_0_ROUTES_0_SERVICEURL: /
zaas-service-2:
image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }}
env:
APIML_SECURITY_X509_ENABLED: true
APIML_SECURITY_X509_ACCEPTFORWARDEDCERT: true
APIML_SECURITY_X509_CERTIFICATESURL: https://gateway-service-2:10010/gateway/certificates
APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10011/eureka/
APIML_HEALTH_PROTECTED: false
APIML_SERVICE_HOSTNAME: zaas-service-2

steps:
- uses: actions/checkout@v4
with:
Expand All @@ -1388,6 +1417,9 @@ jobs:
~/.cache/Cypress
api-catalog-ui/frontend/node_modules
key: my-cache-${{ runner.os }}-${{ hashFiles('api-catalog-ui/frontend/*.json') }}
- name: Run startup check
run: >
./gradlew runStartUpCheck --info --scan -Denvironment.config=-ha -Ddiscoverableclient.instances=1
- name: Cypress run API Catalog
run: |
cd api-catalog-ui/frontend
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.converters.jackson.EurekaJsonJacksonCodec;
import com.netflix.discovery.shared.Application;
import com.netflix.discovery.shared.Applications;
import jakarta.validation.constraints.NotBlank;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.core5.http.Header;
Expand All @@ -30,16 +32,19 @@
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.zowe.apiml.apicatalog.discovery.DiscoveryConfigProperties;
import org.zowe.apiml.constants.EurekaMetadataDefinition;
import org.zowe.apiml.message.log.ApimlLogger;
import org.zowe.apiml.product.instance.InstanceInitializationException;
import org.zowe.apiml.product.logging.annotations.InjectApimlLogger;
import org.zowe.apiml.product.registry.ApplicationWrapper;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;

import static org.zowe.apiml.product.constants.CoreService.GATEWAY;

/**
* Service for instance retrieval from Eureka
Expand All @@ -58,40 +63,63 @@ public class InstanceRetrievalService {
@InjectApimlLogger
private final ApimlLogger apimlLog = ApimlLogger.empty();

private ObjectMapper mapper = new ObjectMapper();

@Autowired
public InstanceRetrievalService(DiscoveryConfigProperties discoveryConfigProperties,
CloseableHttpClient httpClient) {
this.discoveryConfigProperties = discoveryConfigProperties;
this.httpClient = httpClient;
}

/**
* Retrieves {@link InstanceInfo} of particular service
*
* @param serviceId the service to search for
* @return service instance
*/
public InstanceInfo getInstanceInfo(@NotBlank(message = "Service Id must be supplied") String serviceId) {
if (serviceId.equalsIgnoreCase(UNKNOWN)) {
return null;
}

private InstanceInfo getInstanceInfo(String serviceId, AtomicBoolean instanceFound, Predicate<InstanceInfo> selector) {
List<EurekaServiceInstanceRequest> eurekaServiceInstanceRequests = constructServiceInfoQueryRequest(serviceId, false);
// iterate over list of discovery services, return at first success
for (EurekaServiceInstanceRequest eurekaServiceInstanceRequest : eurekaServiceInstanceRequests) {
// call Eureka REST endpoint to fetch single or all Instances
try {
String responseBody = queryDiscoveryForInstances(eurekaServiceInstanceRequest);
if (responseBody != null) {
return extractSingleInstanceFromApplication(serviceId, responseBody);
instanceFound.set(true);
return extractSingleInstanceFromApplication(serviceId, responseBody, selector);
}
} catch (Exception e) {
log.debug("Error obtaining instance information from {}, error message: {}",
eurekaServiceInstanceRequest.getEurekaRequestUrl(), e.getMessage());
}
}
String msg = "An error occurred when trying to get instance info for: " + serviceId;
throw new InstanceInitializationException(msg);
return null;
}

/**
* Retrieves {@link InstanceInfo} of particular service
*
* @param serviceId the service to search for
* @return service instance
*/
public InstanceInfo getInstanceInfo(@NotBlank(message = "Service Id must be supplied") String serviceId) {
if (serviceId.equalsIgnoreCase(UNKNOWN)) {
return null;
}

// identification if there was no instance or any error happened during fetching
AtomicBoolean instanceFound = new AtomicBoolean(false);
InstanceInfo instanceInfo = getInstanceInfo(serviceId, instanceFound,
ii -> EurekaMetadataDefinition.RegistrationType.of(ii.getMetadata()).isPrimary()
);
if (instanceInfo == null) {
// maybe the input is apimlId, try to find the matching Gateway (multi-tenancy use case)
instanceInfo = getInstanceInfo(GATEWAY.getServiceId(), instanceFound,
ii -> EurekaMetadataDefinition.RegistrationType.of(ii.getMetadata()).isAdditional()
);
}

if (!instanceFound.get()) {
String msg = "An error occurred when trying to get instance info for: " + serviceId;
throw new InstanceInitializationException(msg);
}

return instanceInfo;
}

/**
Expand Down Expand Up @@ -141,7 +169,7 @@ private Applications extractApplications(String responseBody) {
* @param eurekaServiceInstanceRequest information used to query the discovery service
* @return ResponseEntity<String> query response
*/
private String queryDiscoveryForInstances(EurekaServiceInstanceRequest eurekaServiceInstanceRequest) throws IOException {
String queryDiscoveryForInstances(EurekaServiceInstanceRequest eurekaServiceInstanceRequest) throws IOException {
HttpGet httpGet = new HttpGet(eurekaServiceInstanceRequest.getEurekaRequestUrl());
for (Header header : createRequestHeader(eurekaServiceInstanceRequest)) {
httpGet.setHeader(header);
Expand Down Expand Up @@ -176,25 +204,22 @@ private String queryDiscoveryForInstances(EurekaServiceInstanceRequest eurekaSer
* @param responseBody the fetch attempt response body
* @return service instance
*/
private InstanceInfo extractSingleInstanceFromApplication(String serviceId, String responseBody) {
InstanceInfo extractSingleInstanceFromApplication(String serviceId, String responseBody, Predicate<InstanceInfo> selector) {
ApplicationWrapper application = null;
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
try {
application = mapper.readValue(responseBody, ApplicationWrapper.class);
} catch (IOException e) {
log.debug("Could not extract service: {} info from discovery --{}", serviceId, e.getMessage(), e);
}


if (application != null
&& application.getApplication() != null
&& application.getApplication().getInstances() != null
&& !application.getApplication().getInstances().isEmpty()) {
return application.getApplication().getInstances().get(0);
} else {
return null;
}
return Optional.ofNullable(application)
.map(ApplicationWrapper::getApplication)
.map(Application::getInstances)
.orElse(Collections.emptyList())
.stream().filter(selector)
.findFirst()
.orElse(null);
}

/**
Expand All @@ -221,7 +246,7 @@ private List<EurekaServiceInstanceRequest> constructServiceInfoQueryRequest(Stri

log.debug("Querying instance information of the service {} from the URL {} with the user {} and password {}",
serviceId, discoveryServiceLocatorUrl, eurekaUsername,
eurekaUserPassword.isEmpty() ? "NO PASSWORD" : "*******");
StringUtils.isEmpty(eurekaUserPassword) ? "NO PASSWORD" : "*******");

EurekaServiceInstanceRequest eurekaServiceInstanceRequest = EurekaServiceInstanceRequest.builder()
.serviceId(serviceId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.netflix.discovery.shared.Application;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.zowe.apiml.apicatalog.model.APIContainer;
Expand All @@ -24,7 +25,6 @@
import org.zowe.apiml.config.ApiInfo;
import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser;
import org.zowe.apiml.message.log.ApimlLogger;
import org.zowe.apiml.product.constants.CoreService;
import org.zowe.apiml.product.logging.annotations.InjectApimlLogger;
import org.zowe.apiml.product.routing.RoutedServices;
import org.zowe.apiml.product.routing.ServiceType;
Expand All @@ -35,6 +35,7 @@
import java.util.concurrent.atomic.AtomicBoolean;

import static org.zowe.apiml.constants.EurekaMetadataDefinition.*;
import static org.zowe.apiml.product.constants.CoreService.GATEWAY;

/**
* Caching service for eureka services
Expand Down Expand Up @@ -67,8 +68,7 @@ public class CachedProductFamilyService {

public CachedProductFamilyService(CachedServicesService cachedServicesService,
TransformService transformService,
@Value("${apiml.service-registry.cacheRefreshUpdateThresholdInMillis}")
Integer cacheRefreshUpdateThresholdInMillis,
@Value("${apiml.service-registry.cacheRefreshUpdateThresholdInMillis}") Integer cacheRefreshUpdateThresholdInMillis,
CustomStyleConfig customStyleConfig) {
this.cachedServicesService = cachedServicesService;
this.transformService = transformService;
Expand Down Expand Up @@ -275,7 +275,7 @@ private String getInstanceHomePageUrl(InstanceInfo instanceInfo) {
String instanceHomePage = instanceInfo.getHomePageUrl();

//Gateway homePage is used to hold DVIPA address and must not be modified
if (hasHomePage(instanceInfo) && !isGateway(instanceInfo)) {
if (hasHomePage(instanceInfo) && !StringUtils.equalsIgnoreCase(GATEWAY.getServiceId(), instanceInfo.getAppName())) {
instanceHomePage = instanceHomePage.trim();
RoutedServices routes = metadataParser.parseRoutes(instanceInfo.getMetadata());
try {
Expand Down Expand Up @@ -326,10 +326,6 @@ private boolean hasHomePage(InstanceInfo instanceInfo) {
&& !instanceHomePage.isEmpty();
}

private boolean isGateway(InstanceInfo instanceInfo) {
return instanceInfo.getAppName().equalsIgnoreCase(CoreService.GATEWAY.getServiceId());
}

/**
* Create a new container based on information in a new instance
*
Expand Down Expand Up @@ -362,7 +358,7 @@ private APIContainer createNewContainerFromService(String productFamilyId, Insta
* @param instanceInfo the service instance
* @return a APIService object
*/
private APIService createAPIServiceFromInstance(InstanceInfo instanceInfo) {
APIService createAPIServiceFromInstance(InstanceInfo instanceInfo) {
boolean secureEnabled = instanceInfo.isPortEnabled(InstanceInfo.PortType.SECURE);

String instanceHomePage = getInstanceHomePageUrl(instanceInfo);
Expand All @@ -384,8 +380,21 @@ private APIService createAPIServiceFromInstance(InstanceInfo instanceInfo) {
log.info("createApiServiceFromInstance#incorrectVersions {}", ex.getMessage());
}

return new APIService.Builder(instanceInfo.getAppName().toLowerCase())
.title(instanceInfo.getMetadata().get(SERVICE_TITLE))
String serviceId = instanceInfo.getAppName();
String title = instanceInfo.getMetadata().get(SERVICE_TITLE);
if (StringUtils.equalsIgnoreCase(GATEWAY.getServiceId(), serviceId)) {
if (RegistrationType.of(instanceInfo.getMetadata()).isAdditional()) {
// additional registration for GW means domain one, update serviceId with the ApimlId
String apimlId = instanceInfo.getMetadata().get(APIML_ID);
if (apimlId != null) {
serviceId = apimlId;
title += " (" + apimlId + ")";
}
}
}

return new APIService.Builder(StringUtils.lowerCase(serviceId))
.title(title)
.description(instanceInfo.getMetadata().get(SERVICE_DESCRIPTION))
.secured(secureEnabled)
.baseUrl(instanceInfo.getHomePageUrl())
Expand Down
Loading

0 comments on commit 1b6a635

Please sign in to comment.