Here you will find instructions for developing a custom extension for VMware Spring Cloud Gateway for Kubernetes.

Prerequisites

Gateway extensions are packaged as Java archives (JARs), and contain classes which extend the base set of Spring Cloud Gateway for Kubernetes features. This is done by adding custom Spring Cloud Gateway Filter and Predicate Factories, or Global Filters.

The requirements to build a Gateway extension are:

  • Java version 17.
  • Spring Configuration classes must be placed under the com.vmware.scg.extensions package.

Project setup

You can use any IDE and build system with the appropriate dependencies and packaging setup.

Gradle

To create a new extension project using Gradle:

  1. Initialize a new Gradle project of type library, implementation Java and Groovy as build script DLS. When prompted, make sure you set the source package to com.vmware.scg.extensions:

     gradle init
    
  2. Update the build.gradle file for your extension library:

     plugins {
     	id 'java-library'
     }
    
     group = 'com.vmware.scg.extensions'
     version = '0.0.1-SNAPSHOT'
    
     repositories {
     	mavenCentral()
     }
    
     java {
     	toolchain {
     		languageVersion = JavaLanguageVersion.of(17)
     	}
     }
    
     dependencies {
     	implementation platform('org.springframework.boot:spring-boot-dependencies:3.1.12')
     	implementation platform('org.springframework.cloud:spring-cloud-dependencies:2022.0.7')
     	implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    
     	testImplementation 'org.springframework.boot:spring-boot-starter-test'
     	testImplementation 'com.github.tomakehurst:wiremock-jre8-standalone:3.3.1'
     }
    
     test {
     	useJUnitPlatform()
    
     	testLogging {
     		exceptionFormat = 'full'
     	}
     }
    

    Important While other versions may work for development, only Spring Boot version 3.1.12 and Spring Cloud 2022.0.7 are fully supported at runtime.

    Note It's safe to add more dependencies, if they don't cause classpath conflicts with the existing ones. However, it's not recommended to overburden extensions with dependencies, given the possible negative impacts on resource consumption and performance.

  3. Delete any .java files created by the generator.

Maven

To create a new extension project using Maven:

  1. Generate a Maven library archetype. Make sure you set the groupId to com.vmware.scg.extensions.

    mvn archetype:generate \
        -DgroupId=com.vmware.scg.extensions \
        -DarchetypeArtifactId=maven-archetype-quickstart
    
  2. Update the pom.xml file for your extension library:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>3.1.12</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.vmware.scg.extensions</groupId>
        <artifactId>mycustomfilter</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>mycustomfilter</name>
        <description>Spring Cloud Gateway for Kubernetes extension</description>
        <properties>
            <java.version>17</java.version>
            <spring-cloud.version>2022.0.7</spring-cloud.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>com.github.tomakehurst</groupId>
                <artifactId>wiremock-jre8-standalone</artifactId>
                <version>3.3.1</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>${spring-cloud.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    </project>
    

    Important While other versions may work for development, only Spring Boot version 3.1.12 and Spring Cloud 2022.0.7 are fully supported at runtime.

    Note It's safe to add more dependencies, if they don't cause classpath conflicts with the existing ones. However, it's not recommended to overburden extensions with dependencies, given the possible negative impacts on resource consumption and performance.

  3. Delete any .java files created by the generator.

Custom extension example

The following is a simple example of a custom extension. The extension implements a new filter called AddMyCustomHeader, which adds an HTTP header to the request sent to the target service.

For more in-depth examples, such as implementing custom predicates or custom configurations, refer to the Spring Cloud Gateway Developer Guide.

Code sample

To create a custom filter you must implement GatewayFilterFactory as a bean. The simplest way to do this is to extend AbstractGatewayFilterFactory, as shown below:

package com.vmware.scg.extensions.filter;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

@Component
class AddMyCustomHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {

    private static final Logger LOGGER = LoggerFactory.getLogger(AddMyCustomHeaderGatewayFilterFactory.class);
    private static final String MY_HEADER_KEY = "X-My-Header";

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {
            ServerWebExchange updatedExchange = exchange.mutate()
                    .request(request -> request.headers(headers -> {
                        headers.put(MY_HEADER_KEY, List.of("my-header-value"));
                        LOGGER.info("Processed request, added " + MY_HEADER_KEY + " header");
                    }))
                    .build();
            return chain.filter(updatedExchange);
        };
    }
}

In the code, you can see that:

  • We named the filter factory AddMyCustomHeaderGatewayFilterFactory. This will make a filter named AddMyCustomHeader available for use in route configurations. You must ensure your extension name does not collide with any of the existing predicates or filters.
  • The filter will be automatically exposed as a bean via Spring's classpath scanning and the @Component annotation, but for more complex configurations you might alternatively choose to use Spring @Configuration classes.
  • Since this filter does not require any special configuration, extending AbstractGatewayFilterFactory with a configuration type of Object is sufficient for our needs.
  • Inside the apply method of this simple example we only need to add our header. You can be as creative as you like here! See the Spring Cloud Gateway Developer Guide section on Custom GatewayFilter Factories for further information.
  • We add an org.slf4j.Logger to provide traces. These will appear in the Gateway Pod logs.

Testing

To test the extension we'll use Spring Boot conventional tools. Extensions can be tested in isolation - Kubernetes is not required.

First, ensure your project has com.github.tomakehurst:wiremock-jre8-standalone:3.3.1 or higher as a test dependency. We'll use WireMock to simulate a service that responds to routed traffic, and to assert on the requests the service receives.

Next, create a test class like this one:

package com.vmware.scg.extensions.filter;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.matching.EqualToPattern;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.test.web.reactive.server.WebTestClient;

import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.ok;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AddMyCustomHeaderTest {

    final WireMockServer wireMock = new WireMockServer(9090);

    @Autowired
    WebTestClient webTestClient;

    @BeforeAll
    void setUp() {
        wireMock.stubFor(get("/add-header").willReturn(ok()));
        wireMock.start();
    }

    @AfterAll
    void tearDown() {
        wireMock.stop();
    }

    @Test
    void shouldApplyExtensionFilter() {
        webTestClient.get()
                .uri("/add-header")
                .exchange()
                .expectStatus()
                .isOk();

        wireMock.verify(getRequestedFor(urlPathEqualTo("/add-header"))
                .withHeader("X-My-Header", new EqualToPattern("my-header-value")));
    }

    @SpringBootApplication
    public static class GatewayApplication {

        @Bean
        public RouteLocator routes(RouteLocatorBuilder builder,
                                   AddMyCustomHeaderGatewayFilterFactory filterFactory) {
            return builder.routes()
                    .route("test_route", r -> r.path("/add-header/**")
                            .filters(f -> f.filters(filterFactory.apply(new Object())))
                            .uri("http://localhost:9090"))
                    .build();
        }

        public static void main(String[] args) {
            SpringApplication.run(GatewayApplication.class, args);
        }
    }
}

Finally, add this configuration to your application.yaml under src/test/resources:

spring:
  cloud:
    gateway:
      routes:
        - uri: http://localhost:9090
          predicates:
            - Path=/add-header/**
          filters:
            - StripPrefix=0
            - AddMyCustomHeader

Important External configuration files under src/main/resources are not supported yet and may cause issues.

In the code above, you can see that:

  • Since the extension we are building is a library, we need to create a simple Spring Boot app GatewayApplication to initialize a basic context for testing purposes.
  • We apply the filter using the builder.routes().route() method directly in the test.
  • We initialize the webTestClient class-level variable with @AutoConfigureWebTestClient for both REST calls and assertions.

Packaging

Now, you can build the extension jar file holding the necessary code. Use either ./gradlew build or mvn package to obtain the jar under build/libs or target folders.

A extension like in the examples should build a JAR of a few kilobytes in size. If the size is of the order of megabytes, ensure that the respective Spring Boot plugins are not present.

  • For Gradle, completely remove the plugin id 'org.springframework.boot' under the plugins section.
  • For Maven, ensure no plugin with groupId org.springframework.boot and artifactId spring-boot-maven-plugin is configured under build.plugins.

Finally, follow the instructions in the Configuring Extensions guide to fully deploy the extension in a Spring Cloud Gateway for Kubernetes instance.

check-circle-line exclamation-circle-line close-line
Scroll to top icon