This page will explain how to develop a custom Extension for Spring Cloud Gateway for Kubernetes.

Prerequisites

A Gateway Extension is a JAVA JAR package with classes that enhance SCG for Kubernetes features by adding custom Spring Cloud Gateway Filter and Predicate factories, as well as Global Filters.

The requirements to build one are:

  • Java 11 compatible.
  • Spring Configuration classes must be under package com.vmware.scg.extensions.

Project setup

You can use any IDE and build system provided you have the appropriate dependencies and packaging setup.

Gradle

  1. Initialize the Gradle project for a Java library with a Groovy build script. 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'
     sourceCompatibility = '11'
    
     repositories {
     	mavenCentral()
     }
    
     ext {
     	set('springCloudVersion', "2020.0.4")
     	set('springBootVersion', "2.5.4")
     }
    
     dependencies {
     	implementation platform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}")
     	implementation platform("org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}")
     	implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
     	/* Not required for the sample app but will be useful for more complex extensions
     	implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
     	implementation 'org.springframework.boot:spring-boot-starter-security'
     	*/
    
     	testImplementation 'org.springframework.boot:spring-boot-starter-test'
     	// Not required for the sample app but will be useful for more complex extensions
     	// testImplementation 'org.springframework.security:spring-security-test'
     	testImplementation 'com.github.tomakehurst:wiremock:2.27.2'
     }
    
     test {
     	useJUnitPlatform()
    
     	testLogging {
     		exceptionFormat = 'full'
     	}
     }
    
  3. Delete any .java files created by the generator.

Note: While other versions may work for development, only Spring Boot version 2.5.x and Spring Cloud 2020.0.4 are fully supported for runtime.
It's safe to add other dependencies, provided they don't cause classpath issues with the current ones. However, it's not recommended to overload the extensions given the possible impact in resources and performance.

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>2.5.4</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>SCG for K8s extension</description>
     	<properties>
     		<java.version>11</java.version>
     		<spring-cloud.version>2020.0.4</spring-cloud.version>
     	</properties>
     	<dependencies>
     		<dependency>
     			<groupId>org.springframework.cloud</groupId>
     			<artifactId>spring-cloud-starter-gateway</artifactId>
     		</dependency>
     		<!-- Not required for the sample app but will be useful for more complex extensions
     		<dependency>
     			<groupId>org.springframework.boot</groupId>
     			<artifactId>spring-boot-starter-oauth2-client</artifactId>
     		</dependency>
     		<dependency>
     			<groupId>org.springframework.boot</groupId>
     			<artifactId>spring-boot-starter-security</artifactId>
     		</dependency>
     		-->
    
     		<dependency>
     			<groupId>org.springframework.boot</groupId>
     			<artifactId>spring-boot-starter-test</artifactId>
     			<scope>test</scope>
     		</dependency>
     		<!-- Not required for the sample app but will be useful for more complex extensions
     		<dependency>
     			<groupId>org.springframework.security</groupId>
     			<artifactId>spring-security-test</artifactId>
     			<scope>test</scope>
     		</dependency>
     		-->
     		<dependency>
     			<groupId>com.github.tomakehurst</groupId>
     			<artifactId>wiremock</artifactId>
     			<version>2.27.2</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>
    
  3. Delete any .java files created by the generator.

Note: While other versions may work for development, only Spring Boot version 2.5.x and Spring Cloud 2020.0.4 are fully supported for runtime.
It's safe to add other dependencies, provided they don't cause classpath issues with the current ones. However, it's not recommended to overload the extensions given the possible impact in resources and performance.

Custom Extension Example

The following is a simple example of a custom extensions that adds an HTTP header to the request sent to the target service. This will cover the basic development concepts as well as testing to get you started.

For more in-depth information (for example, implementing custom predicates or custom configurations), refer to Spring Cloud Gateway Developer Guide.

Custom Filter example code

You can start creating a custom filter like this.

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
public 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 AddMyCustomHeaderGatewayFilterFactory this will make it available as AddMyCustomHeader under the route configurations. Ensure your extension name does not collide with any of the existing predicates or filters.
  • The filter will be automatically detected using @Component annotation, but for complex configurations you can use normal Spring @Configuration classes.
  • Since we do not require any special configuration, extending AbstractGatewayFilterFactory with Object is enough.
  • Inside the apply method we only need to add our header. In this simple example we are adding it always, but you could be more creative. For example, changing the response status with exchange.getResponse().getStatusCode() and adapting the exchange response.
  • We add a normal org.slf4j.Logger to provide traces, these have no special requirements and will appear in the pod logs.

Testing

To test the extension we can use Spring Boot conventional tools without needing much heavy lifting or Kubernetes.

First, add the test dependency com.github.tomakehurst:wiremock:2.27.2 or higher to you project. We will use WireMockServer to simulate a service that responds to routed traffic, and also to assert what the service receives.

Next, create a test class like this one:

package com.vmware.scg.extensions;

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.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 should_apply_extension_filter() {
        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 {

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

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

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

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

In the code above, you can see that:

  • Since we are building a library, we need to create a fake Spring Boot app GatewayApplication to initialize a basic context.
  • The test configuration application.yaml creates a basic routing configuration to apply our extension AddMyCustomHeader.
  • We are initializing WebTestClient with @AutoConfigureWebTestClient for both REST calls and assertions.

After building the plugin jar file with either ./gradle clean build or mvn clean package, head to Configuring Extensions to fully deploy the extension in a SCG for K8s instance.

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