diff --git a/packages/grpc-js-xds/interop/Dockerfile b/packages/grpc-js-xds/interop/test-client.Dockerfile similarity index 100% rename from packages/grpc-js-xds/interop/Dockerfile rename to packages/grpc-js-xds/interop/test-client.Dockerfile diff --git a/packages/grpc-js-xds/interop/test-server.Dockerfile b/packages/grpc-js-xds/interop/test-server.Dockerfile new file mode 100644 index 00000000..61698f84 --- /dev/null +++ b/packages/grpc-js-xds/interop/test-server.Dockerfile @@ -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 -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" ] diff --git a/packages/grpc-js-xds/interop/xds-interop-server.ts b/packages/grpc-js-xds/interop/xds-interop-server.ts index 9d4f7917..a4aeb5fd 100644 --- a/packages/grpc-js-xds/interop/xds-interop-server.ts +++ b/packages/grpc-js-xds/interop/xds-interop-server.ts @@ -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 { const HOSTNAME = os.hostname(); -function testInfoInterceptor(methodDescriptor: grpc.MethodDefinition, call: grpc.ServerInterceptingCall) { +function testInfoInterceptor(methodDescriptor: grpc.ServerMethodDefinition, call: grpc.ServerInterceptingCallInterface) { const listener: grpc.ServerListener = { onReceiveMetadata: async (metadata, next) => { let attemptNum = 0; @@ -142,11 +145,11 @@ function testInfoInterceptor(methodDescriptor: grpc.MethodDefinition, return new grpc.ServerInterceptingCall(call, responder); }; -const testServiceHandler: Partial = { - EmptyCall: (call, callback) => { +const testServiceHandler = { + EmptyCall: (call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) => { callback(null, {}); }, - UnaryCall: (call, callback) => { + UnaryCall: (call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) => { callback(null, { hostname: HOSTNAME, payload: { @@ -156,17 +159,120 @@ const testServiceHandler: Partial = { } }; +function serverBindPromise(server: grpc.Server, port: string, credentials: grpc.ServerCredentials): Promise { + 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, callback: grpc.sendUnaryData) { + healthImpl.setStatus('', 'SERVING'); + callback(null, {}); + }, + SetNotServing(call: grpc.ServerUnaryCall, callback: grpc.sendUnaryData) { + 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(); } diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index aea89c88..5ad27db9 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -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", diff --git a/packages/grpc-js-xds/scripts/psm-interop-build-node.sh b/packages/grpc-js-xds/scripts/psm-interop-build-node.sh index d52206f0..ab7bad19 100755 --- a/packages/grpc-js-xds/scripts/psm-interop-build-node.sh +++ b/packages/grpc-js-xds/scripts/psm-interop-build-node.sh @@ -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}" } diff --git a/packages/grpc-js/src/index.ts b/packages/grpc-js/src/index.ts index e9f2f696..e7001db1 100644 --- a/packages/grpc-js/src/index.ts +++ b/packages/grpc-js/src/index.ts @@ -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, diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index cb60943c..b187765b 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -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 | 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.'