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

feat: route to service based on header #2600

Merged
merged 31 commits into from
Sep 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
db4d73c
header based predicate
achmelo Sep 21, 2022
acc5056
disable hostname validation, route to correct host
achmelo Sep 23, 2022
40fa02c
prepare GH action for SCG
achmelo Sep 26, 2022
b6c6793
SCG IT
achmelo Sep 27, 2022
cc256cc
configuration file
achmelo Sep 27, 2022
eed7872
relaxed tls validation
achmelo Sep 27, 2022
555c351
run scg tests only
achmelo Sep 27, 2022
c464504
eureka client conf
achmelo Sep 27, 2022
69a5e67
Revert "run scg tests only"
achmelo Sep 27, 2022
84afdb6
store and use jacoco dump
achmelo Sep 27, 2022
3735dd1
dump CGW jacoco only
achmelo Sep 27, 2022
f497591
wait for action to end
achmelo Sep 27, 2022
1fc58dc
exclude tests, typo
achmelo Sep 27, 2022
4cc1a56
proxy route locator unit tests
achmelo Sep 27, 2022
d1c320d
header based predicate
achmelo Sep 21, 2022
a86d9ca
disable hostname validation, route to correct host
achmelo Sep 23, 2022
573ba63
prepare GH action for SCG
achmelo Sep 26, 2022
3ccc83d
SCG IT
achmelo Sep 27, 2022
9e7c3c6
configuration file
achmelo Sep 27, 2022
8694a6e
relaxed tls validation
achmelo Sep 27, 2022
1a69cf2
run scg tests only
achmelo Sep 27, 2022
17b1917
eureka client conf
achmelo Sep 27, 2022
7ec69e4
Revert "run scg tests only"
achmelo Sep 27, 2022
30f281d
store and use jacoco dump
achmelo Sep 27, 2022
f998709
dump CGW jacoco only
achmelo Sep 27, 2022
92ad08e
wait for action to end
achmelo Sep 27, 2022
2d7c782
exclude tests, typo
achmelo Sep 27, 2022
84cb24e
proxy route locator unit tests
achmelo Sep 27, 2022
8b98c97
code smells
achmelo Sep 27, 2022
aa3dddd
Merge remote-tracking branch 'origin/rip/GH2036/scg_to_zuul' into rip…
achmelo Sep 27, 2022
51df0a3
Merge branch 'v2.x.x' into rip/GH2036/scg_to_zuul
achmelo Sep 27, 2022
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
56 changes: 54 additions & 2 deletions .github/workflows/containers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,52 @@ jobs:

- uses: ./.github/actions/teardown

CloudGatewayProxy:
needs: PublishJibContainers
runs-on: ubuntu-latest
container: ubuntu:latest
timeout-minutes: 10

services:
discovery-service:
image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }}
gateway-service:
image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }}
gateway-service-2:
image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }}
env:
APIML_SERVICE_HOSTNAME: gateway-service-2
SERVER_INTERNAL_PORT: 10027
cloud-gateway-service:
image: ghcr.io/balhar-jakub/cloud-gateway-service:${{ github.run_id }}-${{ github.run_number }}

steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.head_ref }}

- uses: ./.github/actions/setup

- name: Run CI Tests
run: >
./gradlew :integration-tests:runCloudGatewayProxyTest --info -Denvironment.config=-docker -Denvironment.offPlatform=true
-Partifactory_user=${{ secrets.ARTIFACTORY_USERNAME }} -Partifactory_password=${{ secrets.ARTIFACTORY_PASSWORD }}

- name: Dump CGW jacoco data
run: >
java -jar ./scripts/jacococli.jar dump --address cloud-gateway-service --port 6310 --destfile ./results/cloud-gateway-service.exec

- name: Store results
uses: actions/upload-artifact@v2
if: always()
with:
name: CloudGatewayProxy-${{ env.JOB_ID }}
path: |
integration-tests/build/reports/**
results/**

- uses: ./.github/actions/teardown

CITestsRegistration:
needs: PublishJibContainers
runs-on: ubuntu-latest
Expand Down Expand Up @@ -138,6 +184,7 @@ jobs:
./gradlew :integration-tests:runRegistrationTests --info -Denvironment.config=-docker -Denvironment.offPlatform=true
-Partifactory_user=${{ secrets.ARTIFACTORY_USERNAME }} -Partifactory_password=${{ secrets.ARTIFACTORY_PASSWORD }}
# Coverage results are not stored in this job as it would not provide much additional data

- name: Store results
uses: actions/upload-artifact@v2
if: always()
Expand Down Expand Up @@ -1162,7 +1209,7 @@ jobs:
- uses: ./.github/actions/teardown

PublishResults:
needs: [CITests,CITestsWithInfinispan,CITestsZosmfRsu2012,CITestsWithRedisReplica,CITestsWithRedisSentinel,CITestsInternalPort]
needs: [CITests,CITestsWithInfinispan,CITestsZosmfRsu2012,CITestsWithRedisReplica,CITestsWithRedisSentinel,CITestsInternalPort,CloudGatewayProxy]
runs-on: ubuntu-latest
timeout-minutes: 15

Expand Down Expand Up @@ -1199,9 +1246,14 @@ jobs:
with:
name: ContainerCITestsInternalPort-${{ env.JOB_ID }}
path: containercitestsinternalport
- uses: actions/download-artifact@v3
with:
name: CloudGatewayProxy-${{ env.JOB_ID }}
path: cloudgatewayproxy

- name: Code coverage and publish results
run: >
./gradlew --info coverage sonarqube -Dresults="containercitests/results,citestswithinfinispan/results,containercitestszosmfrsu2012/results,ContainerCITestsWithRedisReplica/results,ContainerCITestsWithRedisSentinel/results,containercitestsinternalport/results"
./gradlew --info coverage sonarqube -Dresults="containercitests/results,citestswithinfinispan/results,containercitestszosmfrsu2012/results,ContainerCITestsWithRedisReplica/results,ContainerCITestsWithRedisSentinel/results,containercitestsinternalport/results,cloudgatewayproxy/results"
-Psonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_TOKEN
-Partifactory_user=$ARTIFACTORY_USERNAME -Partifactory_password=$ARTIFACTORY_PASSWORD
env:
Expand Down
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ task runRegistrationTests(dependsOn: [":integration-tests:runRegistrationTests"]
group "Integration tests"
}

task runCloudGatewayProxyTest(dependsOn: [":integration-tests:runCloudGatewayProxyTest"]) {
description "Run tests verifying cloud gateway can route to correct gateway"
group "Integration tests"
}

task runOauth2Tests(dependsOn: [":integration-tests:runOauth2Tests"]) {
description "Run tests verifying integration with oauth2 provider(okta)"
group "Integration tests"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,22 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.config.HttpClientCustomizer;
import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties;
import org.springframework.cloud.netflix.eureka.CloudEurekaClient;
import org.springframework.cloud.netflix.eureka.MutableDiscoveryClientOptionalArgs;
import org.springframework.cloud.util.ProxyUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.zowe.apiml.cloudgatewayservice.service.ProxyRouteLocator;
import org.zowe.apiml.cloudgatewayservice.service.RouteLocator;
import org.zowe.apiml.security.HttpsConfig;
import org.zowe.apiml.security.HttpsFactory;
import reactor.netty.tcp.SslProvider;

@Configuration
@Slf4j
Expand Down Expand Up @@ -104,6 +108,13 @@ EurekaJerseyClient getEurekaJerseyClient() {
return factory.createEurekaJerseyClientBuilder(eurekaServerUrl, serviceId).build();
}

@Bean
@ConditionalOnProperty(name = "apiml.security.ssl.nonStrictVerifySslCertificatesOfServices", havingValue = "true")
HttpClientCustomizer apimlCustomizer() {
SslProvider provider = SslProvider.defaultClientProvider();
return httpClient -> httpClient.secure(provider);
}

@Bean(destroyMethod = "shutdown")
@RefreshScope
public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config,@Qualifier("apimlEurekaJerseyClient") EurekaJerseyClient eurekaJerseyClient,
Expand All @@ -125,8 +136,16 @@ public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientCon


@Bean
public RouteLocator discoveryClientRouteDefinitionLocator(
@ConditionalOnProperty(name = "apiml.service.gateway.proxy.enabled", havingValue = "false")
public RouteLocator apimlDiscoveryRouteDefLocator(
ReactiveDiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) {
return new RouteLocator(discoveryClient, properties);
}

@Bean
@ConditionalOnProperty(name = "apiml.service.gateway.proxy.enabled", havingValue = "true")
public RouteLocator proxyRouteDefLocator(
ReactiveDiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) {
return new ProxyRouteLocator(discoveryClient, properties);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient;
import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.expression.Expression;
import org.zowe.apiml.product.routing.RoutedService;

import java.net.URI;
import java.util.LinkedHashMap;
import java.util.Locale;

public class ProxyRouteLocator extends RouteLocator {


public ProxyRouteLocator(ReactiveDiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) {
super(discoveryClient, properties);
}

@Override
protected void setProperties(RouteDefinition routeDefinition, ServiceInstance instance, RoutedService service) {
PredicateDefinition predicate = new PredicateDefinition();
predicate.setName("Header");
predicate.addArg("header", "X-Request-Id");
predicate.addArg("regexp", (instance.getServiceId() + instance.getHost()).toLowerCase(Locale.ROOT));
routeDefinition.getPredicates().add(predicate);
}

@Override
protected RouteDefinition buildRouteDefinition(Expression urlExpr, ServiceInstance serviceInstance, String routeId) {
String serviceId = serviceInstance.getInstanceId();
RouteDefinition routeDefinition = new RouteDefinition();
routeDefinition.setId(getRouteIdPrefix() + serviceId + routeId);
String uri = String.format("%s://%s:%d", serviceInstance.getScheme(), serviceInstance.getHost(), serviceInstance.getPort());
routeDefinition.setUri(URI.create(uri));
// add instance metadata
routeDefinition.setMetadata(new LinkedHashMap<>(serviceInstance.getMetadata()));
return routeDefinition;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.util.StringUtils;
import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser;
import org.zowe.apiml.product.routing.RoutedService;
import reactor.core.publisher.Flux;
Expand All @@ -44,18 +43,14 @@ public class RouteLocator implements RouteDefinitionLocator {

public RouteLocator(ReactiveDiscoveryClient discoveryClient,
DiscoveryLocatorProperties properties) {
this(discoveryClient.getClass().getSimpleName(), properties);
this(properties);
serviceInstances = discoveryClient.getServices()
.flatMap(service -> discoveryClient.getInstances(service).collectList());
}

private RouteLocator(String discoveryClientName, DiscoveryLocatorProperties properties) {
private RouteLocator(DiscoveryLocatorProperties properties) {
this.properties = properties;
if (StringUtils.hasText(properties.getRouteIdPrefix())) {
routeIdPrefix = properties.getRouteIdPrefix();
} else {
routeIdPrefix = discoveryClientName + "_";
}
routeIdPrefix = this.getClass().getSimpleName() + "_";
evalCtxt = SimpleEvaluationContext.forReadOnlyDataBinding().withInstanceMethods().build();
}

Expand All @@ -67,30 +62,39 @@ public Flux<RouteDefinition> getRouteDefinitions() {

EurekaMetadataParser metadataParser = new EurekaMetadataParser();
return serviceInstances.filter(instances -> !instances.isEmpty()).flatMap(Flux::fromIterable)
.collectMap(ServiceInstance::getServiceId)
.collectMap(ServiceInstance::getInstanceId)
// remove duplicates
.flatMapMany(map -> Flux.fromIterable(map.values())).map(instance -> {

List<RoutedService> routedServices = metadataParser.parseToListRoute(instance.getMetadata());
List<RouteDefinition> definitionsForInstance = new ArrayList<>();
for (RoutedService service : routedServices) {
RouteDefinition routeDefinition = buildRouteDefinition(urlExpr, instance, service.getSubServiceId());
PredicateDefinition predicate = new PredicateDefinition();
predicate.setName("Path");
String predicateValue = "/" + instance.getServiceId().toLowerCase() + "/" + service.getGatewayUrl() + "/**";
predicate.addArg("pattern", predicateValue);
routeDefinition.getPredicates().add(predicate);
FilterDefinition filter = new FilterDefinition();
filter.setName("RewritePath");
filter.addArg("regexp", predicateValue.replace("/**", "/?(?<remaining>.*)"));
filter.addArg("replacement", service.getServiceUrl() + "/${remaining}");
routeDefinition.getFilters().add(filter);

setProperties(routeDefinition, instance, service);

definitionsForInstance.add(routeDefinition);
}
return definitionsForInstance;
}).flatMapIterable(list -> list);
}

protected void setProperties(RouteDefinition routeDefinition, ServiceInstance instance, RoutedService service) {
PredicateDefinition predicate = new PredicateDefinition();
predicate.setName("Path");
String predicateValue = "/" + instance.getServiceId().toLowerCase() + "/" + service.getGatewayUrl() + "/**";
predicate.addArg("pattern", predicateValue);
routeDefinition.getPredicates().add(predicate);

FilterDefinition filter = new FilterDefinition();
filter.setName("RewritePath");

filter.addArg("regexp", predicateValue.replace("/**", "/?(?<remaining>.*)"));
filter.addArg("replacement", service.getServiceUrl() + "/${remaining}");
routeDefinition.getFilters().add(filter);

}

protected RouteDefinition buildRouteDefinition(Expression urlExpr, ServiceInstance serviceInstance, String routeId) {
String serviceId = serviceInstance.getServiceId();
RouteDefinition routeDefinition = new RouteDefinition();
Expand All @@ -102,4 +106,8 @@ protected RouteDefinition buildRouteDefinition(Expression urlExpr, ServiceInstan
return routeDefinition;
}

public String getRouteIdPrefix() {
return routeIdPrefix;
}

}
6 changes: 6 additions & 0 deletions cloud-gateway-service/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ apiml:
id: cloud-gateway
port: 10023
hostname: localhost
gateway:
proxy:
enabled: true
security:
ssl:
nonStrictVerifySslCertificatesOfServices: true
server:
port: ${apiml.service.port}
ssl:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class WhenCreateRouteLocator {
void thenIsNotNull() {
ReactiveDiscoveryClient discoveryClient = mock(ReactiveDiscoveryClient.class);
DiscoveryLocatorProperties properties = mock(DiscoveryLocatorProperties.class);
Assertions.assertNotNull(httpConfig.discoveryClientRouteDefinitionLocator(discoveryClient, properties));
Assertions.assertNotNull(httpConfig.apimlDiscoveryRouteDefLocator(discoveryClient, properties));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package org.zowe.apiml.cloudgatewayservice.service;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
Expand All @@ -19,31 +20,62 @@
import reactor.core.publisher.Flux;

import java.util.*;
import java.util.regex.Pattern;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.zowe.apiml.constants.EurekaMetadataDefinition.*;

class RouteLocatorTest {

static Map<String, String> metadata = new HashMap<>();

static {
metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api/v1");
metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/");
}

ServiceInstance instance = new DefaultServiceInstance("gateway-10012", "gateway", "gatewayhost", 10012, true, metadata);
ServiceInstance instance2 = new DefaultServiceInstance("gateway-2-10012", "gateway", "gatewayhost-2", 10012, true, metadata);
ReactiveDiscoveryClient dc = mock(ReactiveDiscoveryClient.class);
DiscoveryLocatorProperties properties = new DiscoveryLocatorProperties();


@Test
void givenServiceWithDefinedMetadata_thenLocateRoutes() {
ReactiveDiscoveryClient dc = mock(ReactiveDiscoveryClient.class);
Flux<String> services = Flux.fromIterable(Collections.singleton("gateway"));
Map<String, String> metadata = new HashMap<>();
metadata.put(ROUTES + ".api-v1." + ROUTES_GATEWAY_URL, "api/v1");
metadata.put(ROUTES + ".api-v1." + ROUTES_SERVICE_URL, "/");
ServiceInstance instance = new DefaultServiceInstance("gateway-10012", "gateway", "localhost", 10012, true, metadata);
Flux<ServiceInstance> serviceInstances = Flux.fromIterable(Collections.singleton(instance));
when(dc.getServices()).thenReturn(services);
when(dc.getInstances("gateway")).thenReturn(serviceInstances);
DiscoveryLocatorProperties properties = new DiscoveryLocatorProperties();
RouteLocator locator = new RouteLocator(dc, properties);
Flux<RouteDefinition> definitionFlux = locator.getRouteDefinitions();
List<RouteDefinition> definitions = definitionFlux.collectList().block();
assertNotNull(definitions);
assertEquals(1, definitions.size());
}

@Nested
class GivenProxyRouteLocator {
@Test
void whenServiceIsMatched_thenCreateRouteWithCorrectPredicate() {
Flux<String> services = Flux.fromIterable(Collections.singleton("gateway"));
List<ServiceInstance> instances = Arrays.asList(instance, instance2);
Flux<ServiceInstance> serviceInstances = Flux.fromIterable(instances);
when(dc.getServices()).thenReturn(services);
when(dc.getInstances("gateway")).thenReturn(serviceInstances);
ProxyRouteLocator locator = new ProxyRouteLocator(dc, properties);
Flux<RouteDefinition> definitionFlux = locator.getRouteDefinitions();
List<RouteDefinition> definitions = definitionFlux.collectList().block();
assertNotNull(definitions);
assertEquals(2, definitions.size());
for (int i = 0; i < definitions.size(); i++) {
RouteDefinition def = definitions.get(i);
String expression = def.getPredicates().get(0).getArgs().get("regexp");
assertTrue(Pattern.matches(expression, instances.get(i).getServiceId() + instances.get(i).getHost()));
}
}
}


}
Loading