Skip to content

Commit

Permalink
Add gRPC reverse proxy server example (#5722)
Browse files Browse the repository at this point in the history
Motivation:

Add gRPC reverse proxy server example

Modifications:

Add gRPC reverse proxy server example with testcontainers.

Result:

- Closes #2353 . 

Co-authored-by: jrhee17 <guins_j@guins.org>
  • Loading branch information
eottabom and jrhee17 authored Jun 26, 2024
1 parent ae095af commit f0ec1ba
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 0 deletions.
19 changes: 19 additions & 0 deletions examples/grpc-envoy/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
plugins {
id 'application'
}

dependencies {
implementation project(':core')
implementation project(':grpc')
implementation libs.testcontainers.junit.jupiter
compileOnly libs.javax.annotation
runtimeOnly libs.slf4j.simple

testImplementation project(':junit5')
testImplementation libs.assertj
testImplementation libs.junit5.jupiter.api
}

application {
mainClass.set('example.armeria.grpc.envoy.Main')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package example.armeria.grpc.envoy;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;

import com.github.dockerjava.api.command.InspectContainerResponse;

import com.linecorp.armeria.common.annotation.Nullable;

// https://github.com/envoyproxy/java-control-plane/blob/eaca1a4380e53b4b6339db4e9ffe0ada5e0b7f8f/server/src/test/java/io/envoyproxy/controlplane/server/EnvoyContainer.java
class EnvoyContainer extends GenericContainer<EnvoyContainer> {

private static final Logger LOGGER = LoggerFactory.getLogger(EnvoyContainer.class);

private static final String CONFIG_DEST = "/etc/envoy/envoy.yaml";
private static final String LAUNCH_ENVOY_SCRIPT = "envoy/launch_envoy.sh";
private static final String LAUNCH_ENVOY_SCRIPT_DEST = "/usr/local/bin/launch_envoy.sh";

static final int ADMIN_PORT = 9901;

private final String config;
@Nullable
private final String sedCommand;

/**
* A {@link GenericContainer} implementation for envoy containers.
*
* @param sedCommand optional sed command which may be used to postprocess the provided {@param config}.
* This parameter will be fed into the command {@code sed -e <sedCommand>}.
* An example command may be {@code "s/foo/bar/g;s/abc/def/g"}.
*/
EnvoyContainer(String config, @Nullable String sedCommand) {
super("envoyproxy/envoy:v1.30.1");
this.config = config;
this.sedCommand = sedCommand;
}

@Override
protected void configure() {
super.configure();

withClasspathResourceMapping(LAUNCH_ENVOY_SCRIPT, LAUNCH_ENVOY_SCRIPT_DEST, BindMode.READ_ONLY);
withClasspathResourceMapping(config, CONFIG_DEST, BindMode.READ_ONLY);

if (sedCommand != null) {
withCommand("/bin/bash", "/usr/local/bin/launch_envoy.sh",
sedCommand, CONFIG_DEST, "-l", "debug");
}

addExposedPort(ADMIN_PORT);
}

@Override
protected void containerIsStarting(InspectContainerResponse containerInfo) {
followOutput(new Slf4jLogConsumer(LOGGER).withPrefix("ENVOY"));

super.containerIsStarting(containerInfo);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package example.armeria.grpc.envoy;

import example.armeria.grpc.envoy.Hello.HelloReply;
import example.armeria.grpc.envoy.Hello.HelloRequest;
import example.armeria.grpc.envoy.HelloServiceGrpc.HelloServiceImplBase;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;

public class HelloService extends HelloServiceImplBase {

@Override
public void hello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
if (request.getName().isEmpty()) {
responseObserver.onError(
Status.FAILED_PRECONDITION.withDescription("Name cannot be empty").asRuntimeException());
} else {
responseObserver.onNext(buildReply(toMessage(request.getName())));
responseObserver.onCompleted();
}
}

static String toMessage(String name) {
return "Hello, " + name + '!';
}

private static HelloReply buildReply(Object message) {
return HelloReply.newBuilder().setMessage(String.valueOf(message)).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package example.armeria.grpc.envoy;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.DockerClientFactory;

import com.linecorp.armeria.common.util.ShutdownHooks;
import com.linecorp.armeria.server.Server;
import com.linecorp.armeria.server.grpc.GrpcService;

public final class Main {

private static final Logger logger = LoggerFactory.getLogger(Main.class);

private static final int serverPort = 8080;
// the port envoy binds to within the container
private static final int envoyPort = 10000;

public static void main(String[] args) {
if (!DockerClientFactory.instance().isDockerAvailable()) {
throw new IllegalStateException("Docker is not available");
}

final Server backendServer = startBackendServer(serverPort);
backendServer.closeOnJvmShutdown();
backendServer.start().join();
logger.info("Serving backend at http://127.0.0.1:{}/", backendServer.activePort());

final EnvoyContainer envoyProxy = configureEnvoy(serverPort, envoyPort);
ShutdownHooks.addClosingTask(envoyProxy::stop);
envoyProxy.start();
final Integer mappedEnvoyPort = envoyProxy.getMappedPort(envoyPort);
logger.info("Serving envoy at http://127.0.0.1:{}/", mappedEnvoyPort);
}

private static Server startBackendServer(int serverPort) {
return Server.builder()
.http(serverPort)
.service(GrpcService.builder()
.addService(new HelloService())
.build())
.build();
}

static EnvoyContainer configureEnvoy(int serverPort, int envoyPort) {
final String sedPattern = String.format("s/SERVER_PORT/%s/g;s/ENVOY_PORT/%s/g", serverPort, envoyPort);
return new EnvoyContainer("envoy/envoy.yaml", sedPattern)
.withExposedPorts(envoyPort);
}

private Main() {}
}
19 changes: 19 additions & 0 deletions examples/grpc-envoy/src/main/proto/hello.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
syntax = "proto3";

package example.grpc.hello;
option java_package = "example.armeria.grpc.envoy";
option java_multiple_files = false;

import "google/api/annotations.proto";

service HelloService {
rpc Hello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}
Empty file.
52 changes: 52 additions & 0 deletions examples/grpc-envoy/src/main/resources/envoy/envoy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
admin:
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: listener_0
address:
socket_address:
address: 0.0.0.0
port_value: ENVOY_PORT
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
http_protocol_options:
enable_trailers: true
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: grpc_service
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: grpc_service
type: STRICT_DNS
lb_policy: ROUND_ROBIN
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http_protocol_options:
enable_trailers: true
load_assignment:
cluster_name: grpc_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: host.testcontainers.internal
port_value: SERVER_PORT
16 changes: 16 additions & 0 deletions examples/grpc-envoy/src/main/resources/envoy/launch_envoy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -Eeuo pipefail

SED_COMMAND=$1

CONFIG=$(cat $2)
CONFIG_DIR=$(mktemp -d)
CONFIG_FILE="$CONFIG_DIR/envoy.yaml"

echo "${CONFIG}" | sed -e "${SED_COMMAND}" > "${CONFIG_FILE}"


shift 2
/usr/local/bin/envoy --drain-time-s 1 -c "${CONFIG_FILE}" "$@"

rm -rf "${CONFIG_DIR}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package example.armeria.grpc.envoy;

import static example.armeria.grpc.envoy.Main.configureEnvoy;
import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.testcontainers.junit.jupiter.Testcontainers;

import com.linecorp.armeria.client.grpc.GrpcClients;
import com.linecorp.armeria.common.SessionProtocol;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.grpc.GrpcService;
import com.linecorp.armeria.testing.junit5.server.ServerExtension;

import example.armeria.grpc.envoy.Hello.HelloReply;
import example.armeria.grpc.envoy.Hello.HelloRequest;

@Testcontainers(disabledWithoutDocker = true)
class GrpcEnvoyProxyTest {

// the port envoy binds to within the container
private static final int ENVOY_PORT = 10000;

@RegisterExtension
static ServerExtension server = new ServerExtension() {
@Override
protected void configure(ServerBuilder sb) throws Exception {
sb.service(GrpcService.builder()
.addService(new HelloService())
.build());
}
};

@ParameterizedTest
@EnumSource(value = SessionProtocol.class, names = {"H1C", "H2C"})
void reverseProxy(SessionProtocol sessionProtocol) {
org.testcontainers.Testcontainers.exposeHostPorts(server.httpPort());
try (EnvoyContainer envoy = configureEnvoy(server.httpPort(), ENVOY_PORT)) {
envoy.start();
final String uri = sessionProtocol.uriText() + "://" + envoy.getHost() +
':' + envoy.getMappedPort(ENVOY_PORT);
final HelloServiceGrpc.HelloServiceBlockingStub helloService =
GrpcClients.builder(uri)
.build(HelloServiceGrpc.HelloServiceBlockingStub.class);
final HelloReply reply =
helloService.hello(HelloRequest.newBuilder()
.setName("Armeria")
.build());
assertThat(reply.getMessage()).isEqualTo("Hello, Armeria!");
}
}
}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ includeWithFlags ':examples:graphql-kotlin-example', 'java17', 'ko
project(':examples:graphql-kotlin-example').projectDir = file('examples/graphql-kotlin')
includeWithFlags ':examples:graphql-sangria-example', 'java11', 'scala_2.13'
project(':examples:graphql-sangria-example').projectDir = file('examples/graphql-sangria')
includeWithFlags ':examples:grpc-envoy', 'java11'
includeWithFlags ':examples:grpc-example', 'java11'
project(':examples:grpc-example').projectDir = file('examples/grpc')
includeWithFlags ':examples:grpc-kotlin', 'java11', 'kotlin-grpc', 'kotlin'
Expand Down

0 comments on commit f0ec1ba

Please sign in to comment.