Skip to content

Commit

Permalink
fix(adoption): allow set managed hosts from http header
Browse files Browse the repository at this point in the history
  • Loading branch information
winguse committed Dec 29, 2017
1 parent 2b6e464 commit 5b7cf98
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 12 deletions.
11 changes: 11 additions & 0 deletions README.md
Expand Up @@ -268,6 +268,17 @@ new EnvoyContext({
* For easier migrate service to envoy step by step, we can route traffic to envoy for those service
* migrated. Fill this set for the migrated service.
* This field is default to `undefined` which means all traffic will be route to envoy.
* If this field is set to `undefined`, this library will also try to read it from `x-tubi-envoy-managed-host`.
* You can set in envoy config, like this:
*
* ```yaml
* request_headers_to_add:
* - key: x-tubi-envoy-managed-host
* value: hostname:12345
* - key: x-tubi-envoy-managed-host
* value: foo.bar:8080
* ```
*
* If you set this to be an empty set, then no traffic will be route to envoy.
*/
envoyManagedHosts: new Set(["some-hostname:8080"]);
Expand Down
51 changes: 46 additions & 5 deletions src/envoy-context.ts
Expand Up @@ -42,6 +42,12 @@ export const X_TUBI_ENVOY_EGRESS_PORT = "x-tubi-envoy-egress-port";
*/
export const X_TUBI_ENVOY_EGRESS_ADDR = "x-tubi-envoy-egress-addr";

/**
* the optional header set in envoy config for telling a host is managed by envoy
* so that this library can route envoy or call directly accordingly
*/
export const X_TUBI_ENVOY_MANAGED_HOST = "x-tubi-envoy-managed-host";

/**
* read value of the key from meata
* return undefined if not found or empty
Expand All @@ -57,6 +63,11 @@ export function readMetaAsStringOrUndefined(meta: Metadata, key: string) {
return undefined;
}

function alwaysReturnArray(input: string | string[]) {
if (Array.isArray(input)) return input;
return [input];
}

/**
* read value of the key from header
* return undefined if not found or empty
Expand All @@ -69,10 +80,7 @@ export function readHeaderOrUndefined(header: HttpHeader, key: string) {
if (!value) {
return undefined;
}
if (Array.isArray(value)) {
return value[0];
}
return value;
return alwaysReturnArray(value)[0];
}

/**
Expand Down Expand Up @@ -124,6 +132,17 @@ export interface EnvoyContextInit extends Object {
* For easier migrate service to envoy step by step, we can route traffic to envoy for those service
* migrated. Fill this set for the migrated service.
* This field is default to `undefined` which means all traffic will be route to envoy.
* If this field is set to `undefined`, this library will also try to read it from `x-tubi-envoy-managed-host`.
* You can set in envoy config, like this:
*
* ```yaml
* request_headers_to_add:
* - key: x-tubi-envoy-managed-host
* value: hostname:12345
* - key: x-tubi-envoy-managed-host
* value: foo.bar:8080
* ```
*
* If you set this to be an empty set, then no traffic will be route to envoy.
*/
envoyManagedHosts?: Set<string>;
Expand Down Expand Up @@ -266,6 +285,17 @@ export default class EnvoyContext {
* For easier migrate service to envoy step by step, we can route traffic to envoy for those service
* migrated. Fill this set for the migrated service.
* This field is default to `undefined` which means all traffic will be route to envoy.
* If this field is set to `undefined`, this library will also try to read it from `x-tubi-envoy-managed-host`.
* You can set in envoy config, like this:
*
* ```yaml
* request_headers_to_add:
* - key: x-tubi-envoy-managed-host
* value: hostname:12345
* - key: x-tubi-envoy-managed-host
* value: foo.bar:8080
* ```
*
* If you set this to be an empty set, then no traffic will be route to envoy.
*/
private readonly envoyManagedHosts?: Set<string>;
Expand All @@ -286,6 +316,7 @@ export default class EnvoyContext {
let expectedRequestTimeoutString: string | undefined;
let envoyEgressAddrFromHeader: string | undefined;
let envoyEgressPortStringFromHeader: string | undefined;
let envoyManagedHostsFromHeader: string[] | undefined;

if (meta instanceof Metadata) {
const metadata: Metadata = meta;
Expand All @@ -306,6 +337,10 @@ export default class EnvoyContext {
metadata,
X_TUBI_ENVOY_EGRESS_PORT
);
const managedHosts = metadata.get(X_TUBI_ENVOY_MANAGED_HOST);
if (managedHosts && managedHosts.length > 0) {
envoyManagedHostsFromHeader = managedHosts.map(v => v.toString());
}
} else {
const httpHeader: HttpHeader = meta;
this.traceId = readHeaderOrUndefined(httpHeader, X_B3_TRACEID);
Expand All @@ -322,6 +357,10 @@ export default class EnvoyContext {
);
envoyEgressAddrFromHeader = readHeaderOrUndefined(httpHeader, X_TUBI_ENVOY_EGRESS_ADDR);
envoyEgressPortStringFromHeader = readHeaderOrUndefined(httpHeader, X_TUBI_ENVOY_EGRESS_PORT);
const managedHosts = httpHeader[X_TUBI_ENVOY_MANAGED_HOST];
if (managedHosts) {
envoyManagedHostsFromHeader = alwaysReturnArray(managedHosts);
}
}

if (expectedRequestTimeoutString !== undefined && expectedRequestTimeoutString !== "") {
Expand All @@ -343,8 +382,10 @@ export default class EnvoyContext {

if (this.directMode) {
this.envoyManagedHosts = new Set<string>();
} else {
} else if (envoyManagedHosts !== undefined) {
this.envoyManagedHosts = envoyManagedHosts;
} else if (envoyManagedHostsFromHeader !== undefined) {
this.envoyManagedHosts = new Set<string>(envoyManagedHostsFromHeader);
}
}

Expand Down
60 changes: 60 additions & 0 deletions test/grpc.test.ts
Expand Up @@ -327,4 +327,64 @@ describe("GRPC Test", () => {
await server.stop();
}
});

it("should propagate the tracing header directly if the host is not in managed host", async () => {
const CLIENT_TRACE_ID = `client-id-${Math.floor(Math.random() * 65536)}`;
let requestId: string | undefined;
let traceId: string | undefined;
let spanId: string | undefined;

const server = new class extends GrpcTestServer {
constructor() {
super(5, true);
}

async wrapper(call: ServerUnaryCall): Promise<any> {
const init: EnvoyContextInit = {
meta: call.metadata
};
const innerClient = new PingEnvoyClient(
`${GrpcTestServer.bindHost}:${this.envoyIngressPort}`,
new EnvoyContext(init)
);
const ctx = innerClient.envoyContext;
expect(ctx.clientTraceId).toBe(CLIENT_TRACE_ID);
requestId = ctx.requestId;
traceId = ctx.traceId;
spanId = ctx.spanId;
return innerClient.inner({ message: call.request.message });
}

async inner(call: ServerUnaryCall): Promise<any> {
const ctx = new EnvoyContext(call.metadata);
expect(ctx.clientTraceId).toBe(CLIENT_TRACE_ID);
expect(ctx.requestId).toBe(requestId);
expect(ctx.traceId).toBe(traceId);
expect(ctx.spanId).toBe(spanId);
return { message: "pong" };
}
}();

await server.start();

try {
const clientMetadata = new grpc.Metadata();
clientMetadata.add("x-client-trace-id", CLIENT_TRACE_ID);
const client = new Ping(
`${GrpcTestServer.bindHost}:${server.envoyIngressPort}`,
grpc.credentials.createInsecure()
);
const response = await new Promise((resolve, reject) => {
client.wrapper({ message: "ping" }, clientMetadata, (err: ServiceError, response: any) => {
if (err) {
reject(err);
return;
}
resolve(response);
});
});
} finally {
await server.stop();
}
});
});
56 changes: 55 additions & 1 deletion test/http.test.ts
Expand Up @@ -249,7 +249,7 @@ describe("HTTP Test", () => {

const server = new class extends HttpTestServer {
constructor() {
super(10);
super(14);
}

async wrapper(request: Request): Promise<any> {
Expand Down Expand Up @@ -295,4 +295,58 @@ describe("HTTP Test", () => {
await server.stop();
}
});

it("should propagate the tracing header directly in direct mode", async () => {
const CLIENT_TRACE_ID = `client-id-${Math.floor(Math.random() * 65536)}`;
let requestId: string | undefined;
let traceId: string | undefined;
let spanId: string | undefined;

const server = new class extends HttpTestServer {
constructor() {
super(15, true);
}

async wrapper(request: Request): Promise<any> {
const init: EnvoyContextInit = {
meta: request.headers as HttpHeader
};
const ctx = new EnvoyContext(init);
const client = new EnvoyHttpClient(ctx);
// asserts
expect(ctx.clientTraceId).toBe(CLIENT_TRACE_ID);
requestId = ctx.requestId;
traceId = ctx.traceId;
spanId = ctx.spanId;
// send request to inner
return client.post(
`http://${HttpTestServer.bindHost}:${this.envoyIngressPort}/inner`,
request.body
);
}

async inner(request: Request): Promise<any> {
expect(request.body.message).toBe("ping");
const ctx = new EnvoyContext(request.headers as HttpHeader);
expect(ctx.clientTraceId).toBe(CLIENT_TRACE_ID);
expect(ctx.requestId).toBe(requestId);
expect(ctx.traceId).toBe(traceId);
expect(ctx.spanId).toBe(spanId);
return { message: "pong" };
}
}();

await server.start();

try {
const response = await simplePost(
`http://${HttpTestServer.bindHost}:${server.envoyIngressPort}/wrapper`,
{ message: "ping" },
{ "x-client-trace-id": CLIENT_TRACE_ID }
);
expect(response.message).toBe("pong");
} finally {
await server.stop();
}
});
});
14 changes: 12 additions & 2 deletions test/lib/common-test-server.ts
Expand Up @@ -22,11 +22,12 @@ export default abstract class CommonTestServer {
readonly envoyAdminPort: number;
readonly envoyConfigTemplate: string;
readonly envoyConfigFileName: string;
readonly useManagedHostHeader: boolean;

envoyStdout = "";
envoyStderr = "";

constructor(envoyConfigTemplate: string, serverId: number) {
constructor(envoyConfigTemplate: string, serverId: number, useManagedHostHeader: boolean) {
let port = TEST_PORT_START + serverId * 10;
this.servicePort = port++;
this.envoyIngressPort = port++;
Expand All @@ -36,10 +37,11 @@ export default abstract class CommonTestServer {
this.zipkin = new ZipkinMock(zipkinPort);
this.envoyConfigTemplate = `${__dirname}/${envoyConfigTemplate}`;
this.envoyConfigFileName = `/tmp/envoy-test-config-${this.servicePort}.yaml`;
this.useManagedHostHeader = useManagedHostHeader;
}

async start() {
const envoyConfig = (await readFile(this.envoyConfigTemplate))
let envoyConfig = (await readFile(this.envoyConfigTemplate))
.toString()
.replace(/INGRESS_PORT/g, `${this.envoyIngressPort}`)
.replace(/EGRESS_PORT/g, `${this.envoyEgressPort}`)
Expand All @@ -48,6 +50,14 @@ export default abstract class CommonTestServer {
.replace(/BIND_HOST/g, `${CommonTestServer.bindHost}`)
.replace(/DOMAIN_NAME/g, `${CommonTestServer.domainName}`)
.replace(/SERVICE_PORT/g, `${this.servicePort}`);
if (this.useManagedHostHeader) {
envoyConfig = envoyConfig.replace(
/# MANAGED_HOST_REPLACEMENT/g,
`- { "key": "x-tubi-envoy-managed-host", "value": "${CommonTestServer.domainName}:${
this.envoyIngressPort
}" }`
);
}
await writeFile(this.envoyConfigFileName, envoyConfig);
this.envoy = spawn("envoy", [
"-c",
Expand Down
1 change: 1 addition & 0 deletions test/lib/envoy-grpc-config.yaml
Expand Up @@ -22,6 +22,7 @@ listeners:
value: "EGRESS_PORT"
- key: x-tubi-envoy-egress-addr
value: BIND_HOST
# MANAGED_HOST_REPLACEMENT
filters:
- name: router
config:
Expand Down
1 change: 1 addition & 0 deletions test/lib/envoy-http-config.yaml
Expand Up @@ -18,6 +18,7 @@ listeners:
value: "EGRESS_PORT"
- key: x-tubi-envoy-egress-addr
value: BIND_HOST
# MANAGED_HOST_REPLACEMENT
filters:
- name: router
config:
Expand Down
4 changes: 2 additions & 2 deletions test/lib/grpc-test-server.ts
Expand Up @@ -48,8 +48,8 @@ function wrapImpl(func: (call: ServerUnaryCall) => Promise<any>) {
export default abstract class GrpcTestServer extends CommonTestServer {
readonly server: grpc.Server;

constructor(serverId: number) {
super("./envoy-grpc-config.yaml", serverId);
constructor(serverId: number, useManagedHostHeader = false) {
super("./envoy-grpc-config.yaml", serverId, useManagedHostHeader);
this.server = new grpc.Server();
this.server.addService(Ping.service, {
wrapper: wrapImpl(this.wrapper.bind(this)),
Expand Down
4 changes: 2 additions & 2 deletions test/lib/http-test-server.ts
Expand Up @@ -17,8 +17,8 @@ function stringifyError(err: Error) {
export default abstract class HttpTestServer extends CommonTestServer {
readonly server: Server;

constructor(serverId: number) {
super("./envoy-http-config.yaml", serverId);
constructor(serverId: number, useManagedHostHeader = false) {
super("./envoy-http-config.yaml", serverId, useManagedHostHeader);
this.server = http.createServer(this.processRequest);
}

Expand Down

0 comments on commit 5b7cf98

Please sign in to comment.