Merge pull request #219 from kjin/google-creds

grpc-js: add google credentials implementation
This commit is contained in:
Kelvin Jin 2018-03-15 09:08:35 -07:00 committed by GitHub
commit cdd1620853
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 120 additions and 64 deletions

View File

@ -12,8 +12,8 @@ export class CallCredentialsFilter extends BaseFilter implements Filter {
}
async sendMetadata(metadata: Promise<Metadata>): Promise<Metadata> {
// TODO(murgatroid99): pass real options to generateMetadata
let credsMetadata = this.credentials.generateMetadata({});
// TODO(kjin): pass real service URL to generateMetadata
let credsMetadata = this.credentials.generateMetadata({ service_url: '' });
let resultMetadata = await metadata;
resultMetadata.merge(await credsMetadata);
return resultMetadata;

View File

@ -2,8 +2,10 @@ import {map, reduce} from 'lodash';
import {Metadata} from './metadata';
export type CallMetadataOptions = { service_url: string; };
export type CallMetadataGenerator =
(options: {}, cb: (err: Error|null, metadata?: Metadata) => void) =>
(options: CallMetadataOptions, cb: (err: Error|null, metadata?: Metadata) => void) =>
void;
/**
@ -15,7 +17,7 @@ export interface CallCredentials {
* Asynchronously generates a new Metadata object.
* @param options Options used in generating the Metadata object.
*/
generateMetadata(options: {}): Promise<Metadata>;
generateMetadata(options: CallMetadataOptions): Promise<Metadata>;
/**
* Creates a new CallCredentials object from properties of both this and
* another CallCredentials object. This object's metadata generator will be
@ -28,7 +30,7 @@ export interface CallCredentials {
class ComposedCallCredentials implements CallCredentials {
constructor(private creds: CallCredentials[]) {}
async generateMetadata(options: {}): Promise<Metadata> {
async generateMetadata(options: CallMetadataOptions): Promise<Metadata> {
let base: Metadata = new Metadata();
let generated: Metadata[] = await Promise.all(
map(this.creds, (cred) => cred.generateMetadata(options)));
@ -46,7 +48,7 @@ class ComposedCallCredentials implements CallCredentials {
class SingleCallCredentials implements CallCredentials {
constructor(private metadataGenerator: CallMetadataGenerator) {}
generateMetadata(options: {}): Promise<Metadata> {
generateMetadata(options: CallMetadataOptions): Promise<Metadata> {
return new Promise<Metadata>((resolve, reject) => {
this.metadataGenerator(options, (err, metadata) => {
if (metadata !== undefined) {
@ -64,7 +66,7 @@ class SingleCallCredentials implements CallCredentials {
}
class EmptyCallCredentials implements CallCredentials {
generateMetadata(options: {}): Promise<Metadata> {
generateMetadata(options: CallMetadataOptions): Promise<Metadata> {
return Promise.resolve(new Metadata());
}

View File

@ -271,9 +271,6 @@ export class Http2CallStream extends Duplex implements CallStream {
let code: Status;
let details = '';
switch (errorCode) {
case http2.constants.NGHTTP2_NO_ERROR:
code = Status.OK;
break;
case http2.constants.NGHTTP2_REFUSED_STREAM:
code = Status.UNAVAILABLE;
break;

View File

@ -16,6 +16,10 @@ export interface UnaryCallback<ResponseType> {
(err: ServiceError|null, value?: ResponseType): void;
}
/**
* A generic gRPC client. Primarily useful as a base class for all generated
* clients.
*/
export class Client {
private readonly [kChannel]: Channel;
constructor(

View File

@ -5,31 +5,73 @@ import { Client } from './client';
import { Status} from './constants';
import { makeClientConstructor, loadPackageDefinition } from './make-client';
import { Metadata } from './metadata';
import { IncomingHttpHeaders } from 'http';
const notImplementedFn = () => { throw new Error('Not implemented'); };
export interface OAuth2Client {
getRequestMetadata: (url: string, callback: (err: Error|null, headers?: { Authorization: string }) => void) => void;
}
/**** Client Credentials ****/
// Using assign only copies enumerable properties, which is what we want
export const credentials = Object.assign({
/**
* Create a gRPC credential from a Google credential object.
* @param googleCredentials The authentication client to use.
* @return The resulting CallCredentials object.
*/
createFromGoogleCredential: (googleCredentials: OAuth2Client): CallCredentials => {
return CallCredentials.createFromMetadataGenerator((options, callback) => {
googleCredentials.getRequestMetadata(options.service_url, (err, headers) => {
if (err) {
callback(err);
return;
}
const metadata = new Metadata();
metadata.add('authorization', headers!.Authorization);
callback(null, metadata);
});
});
},
/**
* Combine a ChannelCredentials with any number of CallCredentials into a
* single ChannelCredentials object.
* @param channelCredentials The ChannelCredentials object.
* @param callCredentials Any number of CallCredentials objects.
* @return The resulting ChannelCredentials object.
*/
combineChannelCredentials: (
channelCredentials: ChannelCredentials,
...callCredentials: CallCredentials[]): ChannelCredentials => {
return callCredentials.reduce((acc, other) => acc.compose(other), channelCredentials);
},
/**
* Combine any number of CallCredentials into a single CallCredentials object.
* @param first The first CallCredentials object.
* @param additional Any number of additional CallCredentials objects.
* @return The resulting CallCredentials object.
*/
combineCallCredentials: (
first: CallCredentials,
...additional: CallCredentials[]): CallCredentials => {
return additional.reduce((acc, other) => acc.compose(other), first);
}
}, ChannelCredentials, CallCredentials);
/**** Metadata ****/
// Metadata
export { Metadata };
// Client credentials
export const credentials = {
createSsl: ChannelCredentials.createSsl,
createFromMetadataGenerator: CallCredentials.createFromMetadataGenerator,
createFromGoogleCredential: notImplementedFn /*TODO*/,
combineChannelCredentials: (first: ChannelCredentials, ...additional: CallCredentials[]) => additional.reduce((acc, other) => acc.compose(other), first),
combineCallCredentials: (first: CallCredentials, ...additional: CallCredentials[]) => additional.reduce((acc, other) => acc.compose(other), first),
createInsecure: ChannelCredentials.createInsecure
};
// Constants
/**** Constants ****/
export {
Status as status
// TODO: Other constants as well
};
// Client
/**** Client ****/
export {
Client,
@ -37,4 +79,9 @@ export {
makeClientConstructor,
makeClientConstructor as makeGenericClientConstructor
};
/**
* Close a Client object.
* @param client The client to close.
*/
export const closeClient = (client: Client) => client.close();

View File

@ -130,7 +130,12 @@ export type GrpcObject = {
[index: string]: GrpcObject | ServiceClientConstructor;
};
export function loadPackageDefinition(packageDef: PackageDefinition) {
/**
* Load a gRPC package definition as a gRPC object hierarchy.
* @param packageDef The package definition object.
* @return The resulting gRPC object.
*/
export function loadPackageDefinition(packageDef: PackageDefinition): GrpcObject {
const result: GrpcObject = {};
for (const serviceFqn in packageDef) {
const service = packageDef[serviceFqn];

View File

@ -5,18 +5,6 @@ import {Metadata} from '../src/metadata';
// Metadata generators
function makeGenerator(props: Array<string>): CallMetadataGenerator {
return (options: {[propName: string]: string}, cb) => {
const metadata: Metadata = new Metadata();
props.forEach((prop) => {
if (options[prop]) {
metadata.add(prop, options[prop]);
}
});
cb(null, metadata);
};
}
function makeAfterMsElapsedGenerator(ms: number): CallMetadataGenerator {
return (options, cb) => {
const metadata = new Metadata();
@ -25,7 +13,11 @@ function makeAfterMsElapsedGenerator(ms: number): CallMetadataGenerator {
};
}
const generateFromName: CallMetadataGenerator = makeGenerator(['name']);
const generateFromServiceURL: CallMetadataGenerator = (options, cb) => {
const metadata: Metadata = new Metadata();
metadata.add('service_url', options.service_url);
cb(null, metadata);
};
const generateWithError: CallMetadataGenerator = (options, cb) =>
cb(new Error());
@ -35,16 +27,16 @@ describe('CallCredentials', () => {
describe('createFromMetadataGenerator', () => {
it('should accept a metadata generator', () => {
assert.doesNotThrow(
() => CallCredentials.createFromMetadataGenerator(generateFromName));
() => CallCredentials.createFromMetadataGenerator(generateFromServiceURL));
});
});
describe('compose', () => {
it('should accept a CallCredentials object and return a new object', () => {
const callCredentials1 =
CallCredentials.createFromMetadataGenerator(generateFromName);
CallCredentials.createFromMetadataGenerator(generateFromServiceURL);
const callCredentials2 =
CallCredentials.createFromMetadataGenerator(generateFromName);
CallCredentials.createFromMetadataGenerator(generateFromServiceURL);
const combinedCredentials = callCredentials1.compose(callCredentials2);
assert.notEqual(combinedCredentials, callCredentials1);
assert.notEqual(combinedCredentials, callCredentials2);
@ -52,9 +44,9 @@ describe('CallCredentials', () => {
it('should be chainable', () => {
const callCredentials1 =
CallCredentials.createFromMetadataGenerator(generateFromName);
CallCredentials.createFromMetadataGenerator(generateFromServiceURL);
const callCredentials2 =
CallCredentials.createFromMetadataGenerator(generateFromName);
CallCredentials.createFromMetadataGenerator(generateFromServiceURL);
assert.doesNotThrow(() => {
callCredentials1.compose(callCredentials2)
.compose(callCredentials2)
@ -67,14 +59,14 @@ describe('CallCredentials', () => {
it('should call the function passed to createFromMetadataGenerator',
async () => {
const callCredentials =
CallCredentials.createFromMetadataGenerator(generateFromName);
CallCredentials.createFromMetadataGenerator(generateFromServiceURL);
let metadata: Metadata;
try {
metadata = await callCredentials.generateMetadata({name: 'foo'});
metadata = await callCredentials.generateMetadata({service_url: 'foo'});
} catch (err) {
throw err;
}
assert.deepEqual(metadata.get('name'), ['foo']);
assert.deepEqual(metadata.get('service_url'), ['foo']);
});
it('should emit an error if the associated metadataGenerator does',
@ -83,7 +75,7 @@ describe('CallCredentials', () => {
CallCredentials.createFromMetadataGenerator(generateWithError);
let metadata: Metadata|null = null;
try {
metadata = await callCredentials.generateMetadata({});
metadata = await callCredentials.generateMetadata({service_url: ''});
} catch (err) {
assert.ok(err instanceof Error);
}
@ -115,13 +107,12 @@ describe('CallCredentials', () => {
expected: ['150', '200', '50', '100']
}
];
const options = {};
// Try each test case and make sure the msElapsed field is as expected
await Promise.all(testCases.map(async (testCase) => {
const {credentials, expected} = testCase;
let metadata: Metadata;
try {
metadata = await credentials.generateMetadata(options);
metadata = await credentials.generateMetadata({service_url: ''});
} catch (err) {
throw err;
}

View File

@ -103,7 +103,7 @@ describe('CallStream', () => {
assert2.afterMustCallsSatisfied(done);
});
it('should end a call with an error if a stream was closed', (done) => {
describe('should end a call with an error if a stream was closed', () => {
const c = http2.constants;
const s = Status;
const errorCodeMapping = {
@ -121,21 +121,31 @@ describe('CallStream', () => {
[c.NGHTTP2_ENHANCE_YOUR_CALM]: s.RESOURCE_EXHAUSTED,
[c.NGHTTP2_INADEQUATE_SECURITY]: s.PERMISSION_DENIED
};
forOwn(errorCodeMapping, (value: Status | null, key) => {
const callStream = new Http2CallStream('foo', callStreamArgs, filterStackFactory);
const http2Stream = new ClientHttp2StreamMock({
payload: Buffer.alloc(0),
frameLengths: []
const keys = Object.keys(errorCodeMapping).map(key => Number(key));
keys.forEach((key) => {
const value = errorCodeMapping[key];
// A null value indicates: behavior isn't specified, so skip this test.
let maybeSkip = (fn: typeof it) => value ? fn : fn.skip;
maybeSkip(it)(`for error code ${key}`, () => {
return new Promise((resolve, reject) => {
const callStream = new Http2CallStream('foo', callStreamArgs, filterStackFactory);
const http2Stream = new ClientHttp2StreamMock({
payload: Buffer.alloc(0),
frameLengths: []
});
callStream.attachHttp2Stream(http2Stream);
callStream.once('status', (status) => {
try {
assert.strictEqual(status.code, value);
resolve();
} catch (e) {
reject(e);
}
});
http2Stream.emit('close', Number(key));
});
});
callStream.attachHttp2Stream(http2Stream);
if (value !== null) {
callStream.once('status', assert2.mustCall((status) => {
assert.strictEqual(status.code, value);
}));
}
http2Stream.emit('streamClosed', Number(key));
});
assert2.afterMustCallsSatisfied(done);
});
it('should have functioning getters', (done) => {