Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
66 changes: 66 additions & 0 deletions .github/workflows/grpc-interceptors.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: grpc-interceptors

on:
push:
branches:
- master
- stable
pull_request:
branches:
- master
- stable

jobs:
grpc_interceptors_test:
name: gRPC interceptors (Go ${{ matrix.go }}, PHP ${{ matrix.php }}, OS ${{matrix.os}})
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
matrix:
php: ["8.5"]
go: [stable]
os: ["ubuntu-latest"]

steps:
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v5 # action page: <https://github.com/actions/setup-go>
with:
go-version: ${{ matrix.go }}

- name: Set up PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2 # action page: <https://github.com/shivammathur/setup-php>
with:
php-version: ${{ matrix.php }}
extensions: sockets

- name: Check out code
uses: actions/checkout@v4

- name: Get Composer Cache Directory
id: composer-cache
run: |
cd tests/php_test_files
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Init Composer Cache # Docs: <https://git.io/JfAKn#php---composer>
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-

- name: Install Composer dependencies
run: cd tests/php_test_files && composer update --prefer-dist --no-progress --ansi

- name: Init Go modules Cache # Docs: <https://git.io/JfAKn#go---modules>
uses: actions/cache@v4
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-

- name: Install Go dependencies
run: go mod download

- name: Run interceptors e2e test
run: cd tests && go test -timeout 20m -v -race -tags=debug grpc_interceptors_test.go
3 changes: 2 additions & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ type Config struct {
TLS *TLS `mapstructure:"tls"`

// Env is environment variables passed to the http pool
Env map[string]string `mapstructure:"env"`
Env map[string]string `mapstructure:"env"`
Interceptors []string `mapstructure:"interceptors"`

GrpcPool *pool.Config `mapstructure:"pool"`
MaxSendMsgSize int64 `mapstructure:"max_send_msg_size"`
Expand Down
22 changes: 22 additions & 0 deletions go.work.sum

Large diffs are not rendered by default.

27 changes: 19 additions & 8 deletions protoc_plugins/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
FROM --platform=${TARGETPLATFORM:-linux/amd64} golang:1.22-alpine as builder
FROM golang:1.26-alpine AS builder

ARG TARGETPLATFORM

ENV CGO_ENABLED=0

WORKDIR /src
COPY protoc_plugins/ .
# https://buf.build/docs/bsr/remote-plugins/custom-plugins/#build-a-docker-image
RUN test "${TARGETPLATFORM}" = "linux/amd64" || (echo "buf plugin image must be built for linux/amd64" && exit 1)

COPY protoc_plugins/go.mod protoc_plugins/go.sum ./
RUN go mod download
RUN go mod tidy

RUN go build -trimpath -ldflags "-s" -o protoc-gen-php-grpc-plugin protoc-gen-php-grpc/main.go
COPY protoc_plugins/ .

RUN go build -trimpath -ldflags "-s -w" -o /out/protoc-gen-php-grpc-plugin ./protoc-gen-php-grpc

FROM scratch

ARG APP_VERSION=""
# Supply real metadata at build time, for example:
# docker build --platform linux/amd64 --build-arg BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" --build-arg APP_VERSION="vX.Y.Z" -f protoc_plugins/Dockerfile .
ARG APP_VERSION="unknown"
ARG BUILD_TIME="unknown"

# Runtime dependencies
LABEL org.opencontainers.image.title="protoc-gen-php-grpc"
LABEL org.opencontainers.image.description="protoc plugin for generating PHP gRPC service stubs"
LABEL org.opencontainers.image.url="https://roadrunner.dev"
LABEL org.opencontainers.image.source="https://github.com/roadrunner-server/grpc"
LABEL org.opencontainers.image.vendor="SpiralScout"
LABEL org.opencontainers.image.version="$APP_VERSION"
LABEL org.opencontainers.image.created="$BUILD_TIME"
LABEL org.opencontainers.image.version="${APP_VERSION}"
LABEL org.opencontainers.image.created="${BUILD_TIME}"
LABEL org.opencontainers.image.licenses="MIT"

COPY --from=builder /src/protoc-gen-php-grpc-plugin /
COPY --from=builder --chown=65532:65532 /out/protoc-gen-php-grpc-plugin /protoc-gen-php-grpc-plugin

USER 65532:65532

ENTRYPOINT ["/protoc-gen-php-grpc-plugin"]
4 changes: 1 addition & 3 deletions protoc_plugins/go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
module github.com/roadrunner-server/grpc/protoc_plugins/v5

go 1.23

toolchain go1.23.1
go 1.26

require (
github.com/stretchr/testify v1.11.1
Expand Down
3 changes: 3 additions & 0 deletions protoc_plugins/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down
18 changes: 18 additions & 0 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@
]
}
},
"interceptors": {
"description": "List of registered unary gRPC interceptor plugin names.",
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"minLength": 1
},
"examples": [
[
"sample-grpc-interceptor"
],
[
"auth-interceptor",
"ratelimit-interceptor"
]
]
},
"tls": {
"description": "GRPC TLS configuration",
"type": "object",
Expand Down
20 changes: 15 additions & 5 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,21 @@ func (p *Plugin) createGRPCserver(interceptors map[string]api.Interceptor) (*grp
grpc.UnaryServerInterceptor(p.interceptor),
}

for _, interceptor := range interceptors {
unaryInterceptors = append(
unaryInterceptors,
interceptor.UnaryServerInterceptor(),
)
// if we have interceptors in the config, we need to chain them with our interceptor, and add them to the server options
if len(p.config.Interceptors) > 0 {
// apply interceptors in the same order as they are configured
for i := 0; i < len(p.config.Interceptors); i++ {
name := p.config.Interceptors[i]
if _, ok := interceptors[name]; !ok {
// we should raise an error here, since we may silently ignore let's say auth interceptor, which is critical for security
return nil, errors.E(op, errors.Str(fmt.Sprintf("interceptor %s is not registered", name)))
}

unaryInterceptors = append(
unaryInterceptors,
interceptors[name].UnaryServerInterceptor(),
)
}
}

opts = append(
Expand Down
30 changes: 30 additions & 0 deletions tests/configs/.rr-grpc-rq-interceptors.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
version: '3'
rpc:
listen: "tcp://127.0.0.1:6011"
server:
command: "php php_test_files/worker-grpc.php"
relay: "pipes"
relay_timeout: "20s"
logs:
mode: development
level: error
grpc:
listen: "tcp://127.0.0.1:9011"
proto:
- "proto/service/service.proto"
interceptors:
- "interceptor1"
- "interceptor2"
max_send_msg_size: 50
max_recv_msg_size: 50
max_connection_idle: 0s
max_connection_age: 0s
max_connection_age_grace: 0s
max_concurrent_streams: 10
ping_time: 1s
timeout: 200s
pool:
num_workers: 2
max_jobs: 0
allocate_timeout: 60s
destroy_timeout: 60s
2 changes: 2 additions & 0 deletions tests/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package grpc_test contains end-to-end tests for the gRPC plugin.
package grpc_test
117 changes: 117 additions & 0 deletions tests/grpc_interceptors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package grpc_test

import (
"context"
"log/slog"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"testing"
"time"

"tests/interceptor1"
"tests/interceptor2"
mocklogger "tests/mock"
"tests/proto/service"

"github.com/roadrunner-server/config/v5"
"github.com/roadrunner-server/endure/v2"
grpcPlugin "github.com/roadrunner-server/grpc/v5"
rpcPlugin "github.com/roadrunner-server/rpc/v5"
"github.com/roadrunner-server/server/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)

func TestGrpcInterceptors(t *testing.T) {
cont := endure.New(slog.LevelDebug)

cfg := &config.Plugin{
Version: "2023.3.0",
Path: "configs/.rr-grpc-rq-interceptors.yaml",
}

l, observed := mocklogger.ZapTestLogger(zap.DebugLevel)
err := cont.RegisterAll(
cfg,
&interceptor1.Plugin{},
&interceptor2.Plugin{},
&grpcPlugin.Plugin{},
&rpcPlugin.Plugin{},
&server.Plugin{},
l,
)
assert.NoError(t, err)

err = cont.Init()
if err != nil {
t.Fatal(err)
}

ch, err := cont.Serve()
assert.NoError(t, err)

sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)

wg := &sync.WaitGroup{}
wg.Add(1)

stopCh := make(chan struct{}, 1)

go func() {
defer wg.Done()
for {
select {
case e := <-ch:
assert.Fail(t, "error", e.Error.Error())
err = cont.Stop()
if err != nil {
assert.FailNow(t, "error", err.Error())
}
case <-sig:
err = cont.Stop()
if err != nil {
assert.FailNow(t, "error", err.Error())
}
return
case <-stopCh:
err = cont.Stop()
if err != nil {
assert.FailNow(t, "error", err.Error())
}
return
}
}
}()

time.Sleep(time.Second)

conn, err := grpc.NewClient("127.0.0.1:9011", grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err)
require.NotNil(t, conn)

client := service.NewEchoClient(conn)
resp, err := client.Ping(context.Background(), &service.Message{Msg: "hello"})
require.NoError(t, err)
require.Equal(t, "HELLO", resp.Msg)
_ = conn.Close()

stopCh <- struct{}{}
wg.Wait()

interceptor1Log := observed.FilterMessageSnippet("interceptor1 created marker")
require.Equal(t, 1, interceptor1Log.Len())

interceptor2Log := observed.FilterMessageSnippet("interceptor2 received marker")
require.Equal(t, 1, interceptor2Log.Len())

marker, ok := interceptor2Log.All()[0].ContextMap()["marker"].(string)
require.True(t, ok)
require.True(t, strings.HasPrefix(marker, interceptor1.MarkerPrefix))
}
3 changes: 2 additions & 1 deletion tests/grpc_plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1074,7 +1074,8 @@ func TestGRPCMetrics(t *testing.T) {

func sendReset(address string) func(t *testing.T) {
return func(t *testing.T) {
conn, err := net.Dial("tcp", address)
var d net.Dialer
conn, err := d.DialContext(context.Background(), "tcp", address)
assert.NoError(t, err)
client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
// WorkerList contains list of workers.
Expand Down
2 changes: 2 additions & 0 deletions tests/interceptor1/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package interceptor1 provides a simple test interceptor used in e2e tests.
package interceptor1
Loading
Loading