Skip to content

Commit

Permalink
Add conformance tests for clients on Cloudflare workers (#1066)
Browse files Browse the repository at this point in the history
  • Loading branch information
srikrsna-buf committed May 15, 2024
1 parent d4891c9 commit 9a16b1e
Show file tree
Hide file tree
Showing 13 changed files with 368 additions and 14 deletions.
9 changes: 7 additions & 2 deletions .github/workflows/cloudflare.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
jobs:
test:
if: github.event_name != 'pull_request' || startsWith(github.event.pull_request.title, 'Release ')
runs-on: ubuntu-22.04
runs-on: conformance
steps:
- uses: actions/checkout@v4
- name: cache
Expand All @@ -23,9 +23,14 @@ jobs:
restore-keys: |
${{ runner.os }}-connect-web-ci-
- name: build
run: make build
run: make .tmp/build/connect-conformance
- name: test
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_WORKERS_SERVER_HOST: ${{ vars.CLOUDFLARE_WORKERS_SERVER_HOST}}
CLOUDFLARE_WORKERS_CLIENT_HOST: ${{ vars.CLOUDFLARE_WORKERS_CLIENT_HOST}}
CLOUDFLARE_WORKERS_REFERENCE_SERVER_HOST: ${{ vars.CLOUDFLARE_WORKERS_REFERENCE_SERVER_HOST}}
CLOUDFLARE_WORKERS_REFERENCE_SERVER_KEY: ${{ vars.CLOUDFALRE_WORKERS_REFERENCE_SERVER_KEY }}
CLOUDFLARE_WORKERS_REFERENCE_SERVER_CERT: ${{ vars.CLOUDFALRE_WORKERS_REFERENCE_SERVER_CERT }}
TEMP: .tmp/build
run: make testcloudflareconformance
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ $(BUILD)/connect-node-test: $(BUILD)/connect-node $(BUILD)/connect-fastify $(BUI
@mkdir -p $(@D)
@touch $(@)

$(BUILD)/connect-conformance: $(BUILD)/connect-node $(GEN)/connect-conformance packages/connect-conformance/tsconfig.json $(shell find packages/connect-conformance/src -name '*.ts')
$(BUILD)/connect-conformance: $(BUILD)/connect-node $(BUILD)/connect-web $(GEN)/connect-conformance packages/connect-conformance/tsconfig.json $(shell find packages/connect-conformance/src -name '*.ts')
npm run -w packages/connect-conformance clean
npm run -w packages/connect-conformance build
@mkdir -p $(@D)
Expand Down Expand Up @@ -240,6 +240,7 @@ testwebconformancelocal: $(BUILD)/connect-conformance
.PHONY: testcloudflareconformance
testcloudflareconformance: $(BUILD)/connect-conformance
npm run -w packages/connect-conformance test:cloudflare:server
npm run -w packages/connect-conformance test:cloudflare:client

.PHONY: testwebnode
testwebnode: $(BIN)/node18 $(BIN)/node20 $(BIN)/node21 $(BUILD)/connect-web-test
Expand Down
15 changes: 15 additions & 0 deletions packages/connect-conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,18 @@ Node tests are run as part of regular CI, on every commit. The [src/node](src/no
Cloudflare worker tests are run once every 24 hours and on a release PR. This is because the tests are run on a live Cloudflare worker, and deploying a new version of the worker is a slow process.

The [src/cloudflare](src/cloudflare/) directory contains the entry points for tests.

### Client tests on Cloudflare

Client tests on Cloudflare require a live server with valid TLS. To run the tests, we set up a [self hosted GitHub action runner](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners) that runs the conformance runner and exposes the reference server on 443 over the public internet.

#### Steps to setup the action runner

* Provision a VM with a static public IP address.
* Reserve a domain (can be a subdomain) and point it to the public IP address from the previous step.
* Use [certbot](https://certbot.eff.org/) to setup automatic certificate renewal for the domain.
* Using iptables, redirect traffic from port 443 to a non-privileged port say 8181.
* Create a dedicated user to run the action runner.
* Make sure the user has the necessary permissions to run the conformance runner, read the cert and key that certbot maintains.
* Follow the steps [here](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners) to add a self-hosted runner to the repository.
* Configure the runner app to [run as a service](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service) with the created user.
5 changes: 5 additions & 0 deletions packages/connect-conformance/bin/conformancecloudflareclient
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

const { run } = require("../dist/cjs/cloudflare/client.js");

run();
28 changes: 28 additions & 0 deletions packages/connect-conformance/conformance-cloudflare-client.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
features:
versions:
- HTTP_VERSION_1
- HTTP_VERSION_2
protocols:
- PROTOCOL_CONNECT
# - PROTOCOL_GRPC Cloudflare turns gRPC-Web requests to gRPC, if the invoking request is gRPC. It doesn't directly support gRPC.
- PROTOCOL_GRPC_WEB
codecs:
- CODEC_PROTO
- CODEC_JSON
compressions:
- COMPRESSION_IDENTITY
- COMPRESSION_GZIP
- COMPRESSION_DEFLATE
supportsTls: true
supportsTlsClientCerts: false
supportsConnectGet: true
supportsHalfDuplexBidiOverHttp1: false
supportsMessageReceiveLimit: false
excludeCases:
- useTls: false
- protocol: PROTOCOL_CONNECT
streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM
- protocol: PROTOCOL_GRPC_WEB
streamType: STREAM_TYPE_FULL_DUPLEX_BIDI_STREAM
- compression: COMPRESSION_DEFLATE
streamType: STREAM_TYPE_UNARY
4 changes: 3 additions & 1 deletion packages/connect-conformance/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"conformancenodeclient": "bin/conformancenodeclient",
"conformancewebclient": "bin/conformancewebclient",
"conformancecloudflareserver": "bin/conformancecloudflareserver",
"conformancecloudflareclient": "bin/conformancecloudflareclient",
"connectconformance": "bin/connectconformance"
},
"scripts": {
Expand All @@ -21,7 +22,8 @@
"test:web": "./bin/connectconformance --mode client --conf conformance-web.yaml -v -- ./bin/conformancewebclient",
"test:node:server": "./bin/connectconformance --mode server --conf conformance-node.yaml -v ./bin/conformancenodeserver",
"test:node:client": "./bin/connectconformance --mode client --conf conformance-node.yaml -v ./bin/conformancenodeclient",
"test:cloudflare:server": "npx wrangler deploy -c wrangler-server.toml && ./bin/connectconformance --mode server --conf conformance-cloudflare-server.yaml -v ./bin/conformancecloudflareserver"
"test:cloudflare:server": "npx wrangler deploy -c wrangler-server.toml && ./bin/connectconformance --mode server --conf conformance-cloudflare-server.yaml -v -- ./bin/conformancecloudflareserver",
"test:cloudflare:client": "npx wrangler deploy -c wrangler-client.toml && ./bin/connectconformance --mode client --conf conformance-cloudflare-client.yaml -v --known-failing 'Connect Compressed Error and End-Stream/*/TLS:true/error/compressed' --bind 0.0.0.0 --port 8181 --cert $CLOUDFLARE_WORKERS_REFERENCE_SERVER_CERT --key $CLOUDFLARE_WORKERS_REFERENCE_SERVER_KEY -- ./bin/conformancecloudflareclient"
},
"dependencies": {
"@bufbuild/protobuf": "^1.7.2",
Expand Down
66 changes: 66 additions & 0 deletions packages/connect-conformance/src/cloudflare/client-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2021-2024 The Connect 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.

import { createRegistry } from "@bufbuild/protobuf";

import {
ClientCompatResponse,
ClientErrorResult,
} from "../gen/connectrpc/conformance/v1/client_compat_pb.js";
import { createTransport } from "./transport.js";
import invoke from "../invoke.js";
import {
UnaryRequest,
ServerStreamRequest,
ClientStreamRequest,
BidiStreamRequest,
UnimplementedRequest,
ConformancePayload_RequestInfo,
IdempotentUnaryRequest,
} from "../gen/connectrpc/conformance/v1/service_pb.js";
import { createWorkerHandler } from "./handler.js";
import { InvokeService } from "./invoke-service.js";

export default createWorkerHandler({
jsonOptions: {
typeRegistry: createRegistry(
UnaryRequest,
ServerStreamRequest,
ClientStreamRequest,
BidiStreamRequest,
UnimplementedRequest,
IdempotentUnaryRequest,
ConformancePayload_RequestInfo,
),
},
routes({ service }) {
service(InvokeService, {
async invoke(req) {
const res = new ClientCompatResponse({
testName: req.testName,
});
try {
const invokeResult = await invoke(createTransport(req), req);
res.result = { case: "response", value: invokeResult };
} catch (e) {
res.result = {
case: "error",
value: new ClientErrorResult({ message: (e as Error).message }),
};
}
return res;
},
});
},
});
80 changes: 80 additions & 0 deletions packages/connect-conformance/src/cloudflare/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright 2021-2024 The Connect 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.

import {
ClientCompatRequest,
ClientCompatResponse,
ClientErrorResult,
} from "../gen/connectrpc/conformance/v1/client_compat_pb.js";
import {
createConnectTransport,
createGrpcTransport,
createGrpcWebTransport,
} from "@connectrpc/connect-node";
import { createPromiseClient } from "@connectrpc/connect";
import type { Transport } from "@connectrpc/connect";
import { InvokeService } from "./invoke-service.js";
import { parseArgs } from "node:util";
import {
readSizeDelimitedBuffers,
writeSizeDelimitedBuffer,
} from "../protocol.js";

const { values: flags } = parseArgs({
args: process.argv.slice(2),
options: {
protocol: {
type: "string",
default: "connect",
},
},
});

export async function run() {
const workerUrl = `https://${process.env["CLOUDFLARE_WORKERS_CLIENT_HOST"]}/`;
const transportOptions = { baseUrl: workerUrl, httpVersion: "2" } as const;
let transport: Transport;
switch (flags.protocol) {
case "connect":
transport = createConnectTransport(transportOptions);
break;
case "grpc":
transport = createGrpcTransport(transportOptions);
break;
case "grpc-web":
transport = createGrpcWebTransport(transportOptions);
break;
default:
throw new Error(`Unknown protocol: ${flags.protocol}`);
}
const client = createPromiseClient(InvokeService, transport);
for await (const next of readSizeDelimitedBuffers(process.stdin)) {
const req = ClientCompatRequest.fromBinary(next);
req.host = process.env["CLOUDFLARE_WORKERS_REFERENCE_SERVER_HOST"]!;
let res = new ClientCompatResponse({
testName: req.testName,
});
try {
res = await client.invoke(req);
} catch (e) {
res.result = {
case: "error",
value: new ClientErrorResult({
message: (e as Error).message,
}),
};
}
process.stdout.write(writeSizeDelimitedBuffer(res.toBinary()));
}
}
35 changes: 35 additions & 0 deletions packages/connect-conformance/src/cloudflare/invoke-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2021-2024 The Connect 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.

import { MethodKind } from "@bufbuild/protobuf";
import type { ServiceType } from "@bufbuild/protobuf";
import {
ClientCompatRequest,
ClientCompatResponse,
} from "../gen/connectrpc/conformance/v1/client_compat_pb";

/**
* Used to relay the test request to the worker.
*/
export const InvokeService = {
typeName: "conformance.v1.InvokeService",
methods: {
invoke: {
name: "Invoke",
kind: MethodKind.Unary,
I: ClientCompatRequest,
O: ClientCompatResponse,
},
},
} satisfies ServiceType;
7 changes: 2 additions & 5 deletions packages/connect-conformance/src/cloudflare/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ServerCompatRequest,
ServerCompatResponse,
} from "../gen/connectrpc/conformance/v1/server_compat_pb.js";
import { writeSizeDelimitedBuffer } from "../protocol.js";

export function run() {
const req = ServerCompatRequest.fromBinary(
Expand All @@ -39,9 +40,5 @@ export function run() {
? Buffer.from(tls.rootCertificates.join("\n"))
: undefined,
});
const data = res.toBinary();
const size = Buffer.alloc(4);
size.writeUInt32BE(data.byteLength);
process.stdout.write(size);
process.stdout.write(data);
process.stdout.write(writeSizeDelimitedBuffer(res.toBinary()));
}

0 comments on commit 9a16b1e

Please sign in to comment.