Skip to content

Commit

Permalink
aeon: mock gRPC server
Browse files Browse the repository at this point in the history
Mock server Implement some base methods for integration tests.

Part of #1050, #1049
  • Loading branch information
dmyger committed Dec 23, 2024
1 parent 337b24d commit 6309e21
Show file tree
Hide file tree
Showing 14 changed files with 4,565 additions and 42 deletions.
88 changes: 88 additions & 0 deletions test/integration/aeon/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import os
from pathlib import Path
from signal import SIGQUIT
from subprocess import PIPE, STDOUT, Popen, run

import pytest

from utils import wait_for_lines_in_output


@pytest.fixture(scope="session")
def certificates(tmp_path_factory: pytest.TempPathFactory) -> dict[str, Path]:
dir = tmp_path_factory.mktemp("aeon_cert")
cmd = (Path(__file__).parent / "generate-keys.sh", dir)
returncode = run(cmd).returncode
assert returncode == 0, "Some error on generate certificates"
cert = {
"ca": dir / "ca.crt",
"s_private": dir / "server.key",
"s_public": dir / "server.crt",
"c_private": dir / "client.key",
"c_public": dir / "client.crt",
}
for k, v in cert.items():
assert v.exists(), f"Not found {k} certificate"
return cert


@pytest.fixture(scope="session")
def mock_aeon(tmp_path_factory) -> Path:
server_dir = Path(__file__).parent / "server"
exec = tmp_path_factory.mktemp("aeon_mock") / "aeon"
result = run(f"go build -C {server_dir} -o {exec}".split())
assert result.returncode == 0, "Failed build mock aeon server"
return exec


@pytest.fixture(params=[50052, "@aeon_unix_socket", "AEON"])
def aeon_plain(mock_aeon, tmp_path, request):
cmd = [mock_aeon]
param = request.param
if isinstance(param, int):
cmd.append(f"-port={param}")
elif isinstance(param, str):
if param[0] != "@":
param = tmp_path / param
cmd.append(f"-unix={param}")

aeon = Popen(
cmd,
env=dict(os.environ, GRPC_GO_LOG_SEVERITY_LEVEL="info"),
stderr=STDOUT,
stdout=PIPE,
text=True,
)
print(wait_for_lines_in_output(aeon.stdout, ["ListenSocket created"]))
yield param
aeon.send_signal(SIGQUIT)
assert aeon.wait(5) == 0, "Mock aeon server didn't stopped properly"


@pytest.fixture(params=["server-side", "mutual-tls"])
def aeon_ssl(mock_aeon, certificates, request):
cmd = [
mock_aeon,
"-ssl",
f"-key={certificates['s_private']}",
f"-cert={certificates['s_public']}",
]
mode = request.param
if mode == "mutual-tls":
cmd.append(f"-ca={certificates['ca']}")
elif mode == "server-side":
pass
else:
assert False, "Unsupported TLS mode"

aeon = Popen(
cmd,
env=dict(os.environ, GRPC_GO_LOG_SEVERITY_LEVEL="info"),
stderr=STDOUT,
stdout=PIPE,
text=True,
)
print(wait_for_lines_in_output(aeon.stdout, ["ListenSocket created"]))
yield mode
aeon.send_signal(SIGQUIT)
assert aeon.wait(5) == 0, "Mock aeon ssl server didn't stopped properly"
37 changes: 37 additions & 0 deletions test/integration/aeon/generate-keys.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -e

### First argument is target directory where generate files.
DIR=$(realpath ${1:-$(pwd)})
[ -d $DIR ] || mkdir -p $DIR
cd $DIR

CA_SUBJ="/C=RU/ST=State/L=TestCity/O=Integration test/OU=Aeon/CN=localhost"

cat > ext.cnf << EOF
subjectAltName = @alt_names
[alt_names]
DNS = localhost
IP = 127.0.0.1
EOF

### Server .key&.crt and ca.crt required for Server-Side TLS mode.

# 1. Generate CA's private key and self-signed certificate
openssl req -new -x509 -days 1 -noenc -keyout ca.key -out ca.crt -subj "${CA_SUBJ}" -quiet

# 2. Generate web server's private key and certificate signing request (CSR)
openssl req -new -noenc -keyout server.key -out server.csr -subj "${CA_SUBJ}" -quiet

# 3. Use CA's private key to sign web server's CSR and get back the signed certificate
openssl x509 -req -in server.csr -days 1 -CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -extfile ext.cnf

### Client .key & .crt required for Mutual TSL mode.

# 4. Generate client's private key and certificate signing request (CSR)
openssl req -new -noenc -keyout client.key -out client.csr -subj "$CA_SUBJ" -quiet

# 5. Use CA's private key to sign client's CSR and get back the signed certificate
openssl x509 -req -in client.csr -days 1 -CA ca.crt -CAkey ca.key -CAcreateserial \
-out client.crt -extfile ext.cnf
2 changes: 2 additions & 0 deletions test/integration/aeon/server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.proto
aeon
15 changes: 15 additions & 0 deletions test/integration/aeon/server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Mock `aeon` server

## Update and generate from `.proto` file

1. cd `test/integration/aeon/server`;
2. Get a new [aeon_router.proto](https://github.com/tarantool/aeon/blob/master/proto/aeon_router.proto);
3. Run: `protoc --go_out=. --go-grpc_out=. aeon_router.proto`;
4. Directory `pb` will be created with files:
```
pb
├── aeon_router_grpc.pb.go
└── aeon_router.pb.go
```
5. Commit the new changes in the created files;
6. Update the `service/server.go` file to comply with the new `*.pb.go` requirements.
15 changes: 15 additions & 0 deletions test/integration/aeon/server/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module mock/server/aeon

go 1.22.8

require (
google.golang.org/grpc v1.68.1
google.golang.org/protobuf v1.35.2
)

require (
golang.org/x/net v0.29.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
)
16 changes: 16 additions & 0 deletions test/integration/aeon/server/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
139 changes: 139 additions & 0 deletions test/integration/aeon/server/mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package main

import (
"crypto/tls"
"crypto/x509"
"flag"
"fmt"
"log"
"mock/server/aeon/pb"
"mock/server/aeon/service"
"net"
"os"
"os/signal"
"strings"
"sync"
"syscall"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)

var args = struct {
is_ssl *bool
ca_file *string
cert_file *string
key_file *string
port *int
unix_socket *string
}{
is_ssl: flag.Bool("ssl", false, "Connection uses SSL if set, (default plain TCP)"),
ca_file: flag.String("ca", "", "The CA file"),
cert_file: flag.String("cert", "", "The TLS cert file"),
key_file: flag.String("key", "", "The TLS key file"),
port: flag.Int("port", 50051, "The server port"),
unix_socket: flag.String("unix", "", "The Unix socket name"),
}

func getCertificate() tls.Certificate {
if *args.cert_file == "" || *args.key_file == "" {
log.Fatalln("Both 'key_file' and 'cert_file' required")
}
tls_cert, err := tls.LoadX509KeyPair(*args.cert_file, *args.key_file)
if err != nil {
log.Fatalf("Could not load server key pair: %v", err)
}
return tls_cert
}

func getTlsConfig() *tls.Config {
if *args.ca_file == "" {
return &tls.Config{
Certificates: []tls.Certificate{getCertificate()},
ClientAuth: tls.NoClientCert,
}
}

ca, err := os.ReadFile(*args.ca_file)
if err != nil {
log.Fatalf("Failed to read CA file: %v", err)
}
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(ca) {
log.Fatalln("Failed to append CA data")
}
return &tls.Config{
Certificates: []tls.Certificate{getCertificate()},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: certPool,
}
}

func getServerOpts() []grpc.ServerOption {
if !*args.is_ssl {
return []grpc.ServerOption{}
}
creds := credentials.NewTLS(getTlsConfig())
return []grpc.ServerOption{grpc.Creds(creds)}
}

func getListener() net.Listener {
var protocol string
var address string

if *args.unix_socket != "" {
protocol = "unix"
address = *args.unix_socket
if strings.HasPrefix(address, "@") {
address = "\x00" + address[1:]
}
} else {
protocol = "tcp"
address = fmt.Sprintf("localhost:%d", *args.port)
}
lis, err := net.Listen(protocol, address)
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
return lis
}

func exit() {
log.Println("Exit func")
syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
os.Exit(0)
}

func main() {
log.Println("Start aeon mock server:", os.Args)

flag.Parse()

srv := grpc.NewServer(getServerOpts()...)
pb.RegisterAeonRouterServiceServer(srv, &service.Server{})

// Run gRPC server.
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
if err := srv.Serve(getListener()); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
wg.Done()
}()

// Shutdown on signals.
exit_sig := make(chan os.Signal, 1)
signal.Notify(exit_sig,
syscall.SIGTERM,
syscall.SIGINT,
syscall.SIGQUIT,
syscall.SIGHUP,
)
s := <-exit_sig
log.Println("Got terminate signal:", s)

srv.GracefulStop()
wg.Wait()
log.Println("Exit aeon mock server.")
}
Loading

0 comments on commit 6309e21

Please sign in to comment.