grpc-js-xds: Implement xDS server

This commit is contained in:
Michael Lumish 2025-01-22 14:53:19 -08:00
parent daaa1c37b3
commit a3d99e3554
7 changed files with 166 additions and 10 deletions

View File

@ -0,0 +1,41 @@
# Copyright 2022 gRPC authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Dockerfile for building the xDS interop client. To build the image, run the
# following command from grpc-node directory:
# docker build -t <TAG> -f packages/grpc-js-xds/interop/Dockerfile .
FROM node:18-slim as build
# Make a grpc-node directory and copy the repo into it.
WORKDIR /node/src/grpc-node
COPY . .
WORKDIR /node/src/grpc-node/packages/proto-loader
RUN npm install
WORKDIR /node/src/grpc-node/packages/grpc-js
RUN npm install
WORKDIR /node/src/grpc-node/packages/grpc-js-xds
RUN npm install
FROM gcr.io/distroless/nodejs18-debian11:latest
WORKDIR /node/src/grpc-node
COPY --from=build /node/src/grpc-node/packages/proto-loader ./packages/proto-loader/
COPY --from=build /node/src/grpc-node/packages/grpc-js ./packages/grpc-js/
COPY --from=build /node/src/grpc-node/packages/grpc-js-xds ./packages/grpc-js-xds/
ENV GRPC_VERBOSITY="DEBUG"
ENV GRPC_TRACE=xds_client,server,xds_server
ENTRYPOINT [ "/nodejs/bin/node", "/node/src/grpc-node/packages/grpc-js-xds/build/interop/xds-interop-server" ]

View File

@ -23,9 +23,12 @@ import { ProtoGrpcType } from './generated/test';
import * as protoLoader from '@grpc/proto-loader';
import * as yargs from 'yargs';
import { TestServiceHandlers } from './generated/grpc/testing/TestService';
import * as os from 'os';
import { HealthImplementation } from 'grpc-health-check';
import { Empty__Output, Empty } from './generated/grpc/testing/Empty';
import { SimpleRequest__Output } from './generated/grpc/testing/SimpleRequest';
import { SimpleResponse } from './generated/grpc/testing/SimpleResponse';
import { ReflectionService } from '@grpc/reflection';
const packageDefinition = protoLoader.loadSync('grpc/testing/test.proto', {
keepCase: true,
@ -49,7 +52,7 @@ function setAsyncTimeout(delayMs: number): Promise<void> {
const HOSTNAME = os.hostname();
function testInfoInterceptor(methodDescriptor: grpc.MethodDefinition<any, any>, call: grpc.ServerInterceptingCall) {
function testInfoInterceptor(methodDescriptor: grpc.ServerMethodDefinition<any, any>, call: grpc.ServerInterceptingCallInterface) {
const listener: grpc.ServerListener = {
onReceiveMetadata: async (metadata, next) => {
let attemptNum = 0;
@ -142,11 +145,11 @@ function testInfoInterceptor(methodDescriptor: grpc.MethodDefinition<any, any>,
return new grpc.ServerInterceptingCall(call, responder);
};
const testServiceHandler: Partial<TestServiceHandlers> = {
EmptyCall: (call, callback) => {
const testServiceHandler = {
EmptyCall: (call: grpc.ServerUnaryCall<Empty__Output, Empty>, callback: grpc.sendUnaryData<Empty>) => {
callback(null, {});
},
UnaryCall: (call, callback) => {
UnaryCall: (call: grpc.ServerUnaryCall<SimpleRequest__Output, SimpleResponse>, callback: grpc.sendUnaryData<SimpleResponse>) => {
callback(null, {
hostname: HOSTNAME,
payload: {
@ -156,17 +159,120 @@ const testServiceHandler: Partial<TestServiceHandlers> = {
}
};
function serverBindPromise(server: grpc.Server, port: string, credentials: grpc.ServerCredentials): Promise<number> {
return new Promise((resolve, reject) => {
server.bindAsync(port, credentials, (error, port) => {
if (error) {
reject(error);
} else {
resolve(port);
}
})
})
}
function getIPv4Address(): string | null {
for (const [name, addressList] of Object.entries(os.networkInterfaces())) {
if (name === 'lo' || !addressList) {
continue;
}
for (const address of addressList) {
if (address.family === 'IPv4') {
return address.address;
}
}
}
return null;
}
function main() {
function getIPv6Addresses(): string[] {
const ipv6Addresses: string[] = [];
for (const [name, addressList] of Object.entries(os.networkInterfaces())) {
if (name === 'lo' || !addressList) {
continue;
}
for (const address of addressList) {
if (address.family === 'IPv6') {
ipv6Addresses.push(address.address);
}
}
}
return ipv6Addresses;
}
async function main() {
const argv = yargs
.string(['port', 'maintenance_port', 'address_type'])
.boolean(['secure_mode'])
.choices('address_type', ['IPV4', 'IPV6', 'IPV$_IPV6'])
.demandOption(['port', 'maintenance_port'])
.default('address_type', 'IPV4_IPV6')
.default('secure_mode', false)
.parse()
console.log('Starting xDS interop server. Args: ', argv);
const healthImpl = new HealthImplementation({'': 'SERVING'});
const healthImpl = new HealthImplementation({'': 'NOT_SERVING'});
const xdsUpdateHealthServiceImpl = {
SetServing(call: grpc.ServerUnaryCall<Empty, Empty__Output>, callback: grpc.sendUnaryData<Empty__Output>) {
healthImpl.setStatus('', 'SERVING');
callback(null, {});
},
SetNotServing(call: grpc.ServerUnaryCall<Empty, Empty__Output>, callback: grpc.sendUnaryData<Empty__Output>) {
healthImpl.setStatus('', 'NOT_SERVING');
callback(null, {});
}
}
const reflection = new ReflectionService(packageDefinition, {
services: ['grpc.testing.TestService']
})
if (argv.secure_mode) {
if (argv.address_type !== 'IPV4_IPV6') {
throw new Error('Secure mode only supports IPV4_IPV6 address type');
}
const maintenanceServer = new grpc.Server();
maintenanceServer.addService(loadedProto.grpc.testing.XdsUpdateHealthService.service, xdsUpdateHealthServiceImpl)
healthImpl.addToServer(maintenanceServer);
reflection.addToServer(maintenanceServer);
grpc.addAdminServicesToServer(maintenanceServer);
const server = new grpc_xds.XdsServer({interceptors: [testInfoInterceptor]});
server.addService(loadedProto.grpc.testing.TestService.service, testServiceHandler);
const xdsCreds = new grpc_xds.XdsServerCredentials(grpc.ServerCredentials.createInsecure());
await Promise.all([
serverBindPromise(maintenanceServer, `[::]:${argv.maintenance_port}`, grpc.ServerCredentials.createInsecure()),
serverBindPromise(server, `[::]:${argv.port}`, xdsCreds)
]);
} else {
const server = new grpc.Server({interceptors: [testInfoInterceptor]});
server.addService(loadedProto.grpc.testing.XdsUpdateHealthService.service, xdsUpdateHealthServiceImpl);
healthImpl.addToServer(server);
reflection.addToServer(server);
grpc.addAdminServicesToServer(server);
server.addService(loadedProto.grpc.testing.TestService.service, testServiceHandler);
const creds = grpc.ServerCredentials.createInsecure();
switch (argv.address_type) {
case 'IPV4_IPV6':
await serverBindPromise(server, `[::]:${argv.port}`, creds);
break;
case 'IPV4':
await serverBindPromise(server, `127.0.0.1:${argv.port}`, creds);
const address = getIPv4Address();
if (address) {
await serverBindPromise(server, `${address}:${argv.port}`, creds);
}
break;
case 'IPV6':
await serverBindPromise(server, `[::1]:${argv.port}`, creds);
for (const address of getIPv6Addresses()) {
await serverBindPromise(server, `${address}:${argv.port}`, creds);
}
break;
default:
throw new Error(`Unknown address type: ${argv.address_type}`);
}
}
healthImpl.setStatus('', 'SERVING');
}
if (require.main === module) {
main();
}

View File

@ -38,6 +38,7 @@
"@types/gulp-mocha": "0.0.32",
"@types/mocha": "^5.2.6",
"@types/node": ">=20.11.20",
"@grpc/reflection": "file:../grpc-reflection",
"@types/yargs": "^15.0.5",
"find-free-ports": "^3.1.1",
"grpc-health-check": "file:../grpc-health-check",

View File

@ -28,11 +28,13 @@ set -eo pipefail
# Writes the output of docker image build stdout, stderr
#######################################
psm::lang::build_docker_images() {
local client_dockerfile="packages/grpc-js-xds/interop/Dockerfile"
local client_dockerfile="packages/grpc-js-xds/interop/test-client.Dockerfile"
local server_dockerfile="packages/grpc-js-xds/interop/test-server.Dockerfile"
cd "${SRC_DIR}"
psm::tools::run_verbose git submodule update --init --recursive
psm::tools::run_verbose git submodule status
psm::build::docker_images_generic "${client_dockerfile}"
psm::build::docker_images_generic "${server_dockerfile}"
}

View File

@ -44,6 +44,7 @@ import {
makeClientConstructor,
MethodDefinition,
Serialize,
ServerMethodDefinition,
ServiceDefinition,
} from './make-client';
import { Metadata, MetadataOptions, MetadataValue } from './metadata';
@ -181,6 +182,7 @@ export {
ServerWritableStream,
ServerDuplexStream,
ServerErrorResponse,
ServerMethodDefinition,
ServiceDefinition,
UntypedHandleCall,
UntypedServiceImplementation,

View File

@ -99,6 +99,10 @@ const { HTTP2_HEADER_PATH } = http2.constants;
const TRACER_NAME = 'server';
const kMaxAge = Buffer.from('max_age');
function serverCallTrace(text: string) {
logging.trace(LogVerbosity.DEBUG, 'server_call', text);
}
type AnyHttp2Server = http2.Http2Server | http2.Http2SecureServer;
interface BindResult {
@ -1248,7 +1252,7 @@ export class Server {
}
private _retrieveHandler(path: string): Handler<any, any> | null {
this.trace(
serverCallTrace(
'Received call to method ' +
path +
' at address ' +
@ -1258,7 +1262,7 @@ export class Server {
const handler = this.handlers.get(path);
if (handler === undefined) {
this.trace(
serverCallTrace(
'No handler registered for method ' +
path +
'. Sending UNIMPLEMENTED status.'