Skip to content

Commit

Permalink
Runtime app routing now uses a static route definition and rewrites t…
Browse files Browse the repository at this point in the history
…he to forward in a filter
  • Loading branch information
tgeens committed Sep 6, 2023
1 parent 57c0f5f commit ecbe3e2
Show file tree
Hide file tree
Showing 13 changed files with 430 additions and 71 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ dependencies {

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.springframework.cloud:spring-cloud-contract-wiremock'

testImplementation 'io.rest-assured:rest-assured'
testImplementation 'io.projectreactor:reactor-test'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.contentgrid.gateway.runtime.config.kubernetes.Fabric8SecretMapper;
import com.contentgrid.gateway.runtime.config.kubernetes.KubernetesResourceWatcherBinding;
import com.contentgrid.gateway.runtime.cors.RuntimeCorsConfigurationSource;
import com.contentgrid.gateway.runtime.routing.RuntimeDeploymentGatewayFilter;
import com.contentgrid.gateway.runtime.routing.DefaultRuntimeRequestResolver;
import com.contentgrid.gateway.runtime.routing.DefaultRuntimeRequestRouter;
import com.contentgrid.gateway.runtime.routing.DynamicVirtualHostResolver;
Expand All @@ -33,6 +34,9 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.GatewayFilterSpec;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.cloud.kubernetes.fabric8.loadbalancer.Fabric8ServiceInstanceMapper;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
Expand All @@ -45,12 +49,25 @@
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(value = "contentgrid.gateway.runtime-platform.enabled")
public class RuntimeConfiguration {
@Bean
RuntimeDeploymentGatewayFilter deploymentGatewayFilter() {
return new RuntimeDeploymentGatewayFilter();
}

@Bean
RouteLocator runtimeAppRouteLocator(RouteLocatorBuilder builder, RuntimeRequestResolver requestResolver) {
return builder.routes()
.route(r -> r
.predicate(exchange -> requestResolver.resolveDeploymentId(exchange).isPresent())
.filters(GatewayFilterSpec::preserveHostHeader)
.uri("cg://ignored")
)
.build();
}

@Bean
public ServiceCatalog serviceTracker(ApplicationEventPublisher publisher,
ContentGridDeploymentMetadata deploymentMetadata, ApplicationConfigurationRepository appConfigRepository) {
return new ServiceCatalog(publisher, deploymentMetadata, appConfigRepository);
public ServiceCatalog serviceTracker(ContentGridDeploymentMetadata deploymentMetadata) {
return new ServiceCatalog(deploymentMetadata);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,21 @@

@Slf4j
public class ServiceCatalog implements
ServiceAddedHandler, ServiceDeletedHandler,
RouteDefinitionLocator /* replace this later with DiscoveryClientRouteDefinitionLocator */ {

@NonNull
private final ApplicationEventPublisher publisher;

ServiceAddedHandler, ServiceDeletedHandler
{
@NonNull
private final ContentGridDeploymentMetadata deploymentMetadata;

@NonNull
private final ApplicationConfigurationRepository appConfigRepository;

private final ConcurrentLookup<String, ServiceInstance> services;
@NonNull
private final Lookup<ApplicationId, ServiceInstance> lookupByApplicationId;

@NonNull
private final Lookup<DeploymentId, ServiceInstance> lookupByDeploymentId;

public ServiceCatalog(@NonNull ApplicationEventPublisher publisher,
@NonNull ContentGridDeploymentMetadata deploymentMetadata,
@NonNull ApplicationConfigurationRepository appConfigRepository) {
this.publisher = publisher;
public ServiceCatalog(
@NonNull ContentGridDeploymentMetadata deploymentMetadata
) {
this.deploymentMetadata = deploymentMetadata;
this.appConfigRepository = appConfigRepository;

this.services = new ConcurrentLookup<>(ServiceInstance::getInstanceId);
this.lookupByApplicationId = this.services.createLookup(
Expand All @@ -61,48 +53,14 @@ public ServiceCatalog(@NonNull ApplicationEventPublisher publisher,

}

private RouteDefinition createRouteDefinition(ServiceInstance service) {
var routeDef = new RouteDefinition();
routeDef.setId("k8s-" + service.getServiceId());
routeDef.setUri(service.getUri());

var hostnamePredicate = new PredicateDefinition();
hostnamePredicate.setName("Host");

var domainNames = this.deploymentMetadata.getApplicationId(service)
.map(this.appConfigRepository::getApplicationConfiguration)
.map(ApplicationConfiguration::getDomains)
.orElse(Set.of())
.stream()
// also accept Host headers on the current server port
.flatMap(domain -> Stream.of(domain + ":*", domain))
.toList();

for (int i = 0; i < domainNames.size(); i++) {
hostnamePredicate.addArg("index_"+i, domainNames.get(i));
}

routeDef.setPredicates(List.of(hostnamePredicate));

return routeDef;
}

@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return Flux.fromStream(() -> this.services().map(this::createRouteDefinition))
.log(Loggers.getLogger(ServiceCatalog.class), Level.FINE, false);
}

@Override
public void handleServiceAdded(ServiceInstance service) {
services.add(service);
publisher.publishEvent(new RefreshRoutesEvent(this));
}

@Override
public void handleServiceDeleted(ServiceInstance service) {
services.remove(service.getInstanceId());
publisher.publishEvent(new RefreshRoutesEvent(this));
}

public Stream<ServiceInstance> services() {
Expand All @@ -111,7 +69,7 @@ public Stream<ServiceInstance> services() {

public Collection<ServiceInstance> findByApplicationId(@NonNull ApplicationId applicationId) {
var services = this.lookupByApplicationId.apply(applicationId);
if (log.isDebugEnabled()){
if (log.isDebugEnabled()) {
log.debug("findByApplicationId({}) -> [{}]", applicationId,
services.stream().map(ServiceInstance::getServiceId).collect(Collectors.joining(", ")));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,24 @@
@Slf4j
public class SimpleContentGridDeploymentMetadata implements ContentGridDeploymentMetadata {

public static final String LABEL_APPLICATION_ID = "app.contentgrid.com/application-id";
public static final String LABEL_DEPLOYMENT_ID = "app.contentgrid.com/deployment-id";
public static final String LABEL_POLICY_PACKAGE = "authz.contentgrid.com/policy-package";

public Optional<ApplicationId> getApplicationId(@NonNull ServiceInstance service) {
return Optional.ofNullable(service.getMetadata().get("app.contentgrid.com/application-id"))
return Optional.ofNullable(service.getMetadata().get(LABEL_APPLICATION_ID))
.map(ApplicationId::from);
}

@Override
public Optional<DeploymentId> getDeploymentId(ServiceInstance service) {
return Optional.ofNullable(service.getMetadata().get("app.contentgrid.com/deployment-id"))
return Optional.ofNullable(service.getMetadata().get(LABEL_DEPLOYMENT_ID))
.map(DeploymentId::from);
}

@Override
public Optional<String> getPolicyPackage(ServiceInstance service) {
var policyPackage = service.getMetadata().get("authz.contentgrid.com/policy-package");
var policyPackage = service.getMetadata().get(LABEL_POLICY_PACKAGE);
if (!StringUtils.hasText(policyPackage)) {
log.warn("Service {} (deployment:{}) has no policy package defined",
service.getServiceId(), this.getDeploymentId(service));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.contentgrid.gateway.runtime.routing;

import static com.contentgrid.gateway.runtime.web.ContentGridAppRequestWebFilter.CONTENTGRID_SERVICE_INSTANCE_ATTR;
import static org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter.LOAD_BALANCER_CLIENT_FILTER_ORDER;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR;

import java.net.URI;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.CompletionContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.DelegatingServiceInstance;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Slf4j
@RequiredArgsConstructor
public class RuntimeDeploymentGatewayFilter implements GlobalFilter, Ordered {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

URI routedUri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
if (routedUri == null || !"cg".equals(routedUri.getScheme())) {
return chain.filter(exchange);
}

ServiceInstance serviceInstance = exchange.getAttribute(CONTENTGRID_SERVICE_INSTANCE_ATTR);
if (serviceInstance == null) {
var host = exchange.getRequest().getURI().getHost();
var message = "Unable to find service instance for %s".formatted(host);
throw NotFoundException.create(false /* HTTP 503 */, message);
} else {

// the `<scheme>` for routedUri is `cg://`, so we need to override the default scheme
// if the serviceInstance doesn't provide one.
String overrideScheme = serviceInstance.isSecure() ? "https" : "http";
serviceInstance = new DelegatingServiceInstance(serviceInstance, overrideScheme);

routedUri = LoadBalancerUriTools.reconstructURI(serviceInstance, routedUri);

if (log.isDebugEnabled()) {
log.debug("Routing {} to {}", exchange.getRequest().getURI(), serviceInstance.getHost());
}
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, routedUri);
}

return chain.filter(exchange);
}

@Override
public int getOrder() {
return LOAD_BALANCER_CLIENT_FILTER_ORDER;
}
}
3 changes: 3 additions & 0 deletions src/main/resources/application-bootRun.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ spring:
filters:
- RedirectTo=302, /me

logging.level:
com.contentgrid.gateway.runtime.routing: DEBUG

#opa:
# service:
# url: http://localhost:8181
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -148,10 +149,6 @@ public void testConfiguredServiceDiscoveryHappyPath() {
var appId = ApplicationId.random();
var deploymentId = DeploymentId.random();

List<Route> routes = routeLocator.getRoutes().collectList().block();
assertThat(routes).isNotNull();
assertThat(routes).isEmpty();

webTestClient
.mutateWith(mockOidcLogin())
.get()
Expand Down Expand Up @@ -212,9 +209,6 @@ public void testConfiguredServiceDiscoveryHappyPath() {
.expectBody(String.class)
.value(body -> assertThat(body).isEqualTo("Hello ContentGrid!"));
});

var newRoutes = routeLocator.getRoutes().collectList().block();
assertThat(newRoutes).isNotEmpty();
}
}

Expand Down
Loading

0 comments on commit ecbe3e2

Please sign in to comment.