Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/grpc-js/src/call-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ class ComposedCallCredentials extends CallCredentials {
}

compose(other: CallCredentials): CallCredentials {
if (other instanceof EmptyCallCredentials) {
return this;
}
return new ComposedCallCredentials(this.creds.concat([other]));
}

Expand Down Expand Up @@ -197,6 +200,9 @@ class SingleCallCredentials extends CallCredentials {
}

compose(other: CallCredentials): CallCredentials {
if (other instanceof EmptyCallCredentials) {
return this;
}
return new ComposedCallCredentials([this, other]);
}

Expand Down Expand Up @@ -225,3 +231,9 @@ class EmptyCallCredentials extends CallCredentials {
return other instanceof EmptyCallCredentials;
}
}

export function isEmptyCallCredentials(
callCredentials: CallCredentials
): boolean {
return callCredentials instanceof EmptyCallCredentials;
}
3 changes: 2 additions & 1 deletion packages/grpc-js/src/channel-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ class InsecureChannelCredentialsImpl extends ChannelCredentials {
return other instanceof InsecureChannelCredentialsImpl;
}
_createSecureConnector(channelTarget: GrpcUri, options: ChannelOptions, callCredentials?: CallCredentials): SecureConnector {
const credentials = callCredentials ?? CallCredentials.createEmpty();
return {
connect(socket) {
return Promise.resolve({
Expand All @@ -195,7 +196,7 @@ class InsecureChannelCredentialsImpl extends ChannelCredentials {
return Promise.resolve();
},
getCallCredentials: () => {
return callCredentials ?? CallCredentials.createEmpty();
return credentials;
},
destroy() {}
}
Expand Down
25 changes: 20 additions & 5 deletions packages/grpc-js/src/load-balancing-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*
*/

import { CallCredentials } from './call-credentials';
import { CallCredentials, isEmptyCallCredentials } from './call-credentials';
import {
Call,
DeadlineInfoProvider,
Expand All @@ -38,6 +38,8 @@ import { AuthContext } from './auth-context';
import { SubchannelInterface } from './subchannel-interface';

const TRACER_NAME = 'load_balancing_call';
const RESOLVED_EMPTY_METADATA: Promise<Metadata | undefined> =
Promise.resolve(undefined);

export type RpcProgress = 'NOT_STARTED' | 'DROP' | 'REFUSED' | 'PROCESSED';

Expand Down Expand Up @@ -139,6 +141,17 @@ export class LoadBalancingCall implements Call, DeadlineInfoProvider {
}
}

private generateCallCredentialsMetadata(
callCredentials: CallCredentials
): Promise<Metadata | undefined> {
return isEmptyCallCredentials(callCredentials)
? RESOLVED_EMPTY_METADATA
: callCredentials.generateMetadata({
method_name: this.methodName,
service_url: this.serviceUrl,
});
}

doPick() {
if (this.ended) {
return;
Expand Down Expand Up @@ -167,9 +180,9 @@ export class LoadBalancingCall implements Call, DeadlineInfoProvider {
}
switch (pickResult.pickResultType) {
case PickResultType.COMPLETE:
const combinedCallCredentials = this.credentials.compose(pickResult.subchannel!.getCallCredentials());
combinedCallCredentials
.generateMetadata({ method_name: this.methodName, service_url: this.serviceUrl })
this.generateCallCredentialsMetadata(
this.credentials.compose(pickResult.subchannel!.getCallCredentials())
)
.then(
credsMetadata => {
/* If this call was cancelled (e.g. by the deadline) before
Expand All @@ -181,7 +194,9 @@ export class LoadBalancingCall implements Call, DeadlineInfoProvider {
);
return;
}
finalMetadata.merge(credsMetadata);
if (credsMetadata) {
finalMetadata.merge(credsMetadata);
}
if (finalMetadata.get('authorization').length > 1) {
this.outputStatus(
{
Expand Down
182 changes: 182 additions & 0 deletions packages/grpc-js/test/test-call-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ import * as assert from 'assert';
import {
CallCredentials,
CallMetadataGenerator,
isEmptyCallCredentials,
} from '../src/call-credentials';
import { ConnectivityState } from '../src/connectivity-state';
import { Status } from '../src/constants';
import { LoadBalancingCall } from '../src/load-balancing-call';
import { Metadata } from '../src/metadata';
import { PickResultType } from '../src/picker';
import { computeServiceUrl } from '../src/uri-parser';

// Metadata generators

Expand Down Expand Up @@ -52,6 +58,37 @@ describe('CallCredentials', () => {
});
});

describe('isEmptyCallCredentials', () => {
it('should return true for empty call credentials', () => {
const emptyCredentials = CallCredentials.createEmpty();
assert.strictEqual(isEmptyCallCredentials(emptyCredentials), true);
assert.strictEqual(
isEmptyCallCredentials(emptyCredentials.compose(emptyCredentials)),
true
);
});

it('should return false for non-empty call credentials', () => {
const callCredentials1 = CallCredentials.createFromMetadataGenerator(
generateFromServiceURL
);
const callCredentials2 = CallCredentials.createFromMetadataGenerator(
generateFromServiceURL
);
assert.strictEqual(isEmptyCallCredentials(callCredentials1), false);
assert.strictEqual(
isEmptyCallCredentials(
callCredentials1.compose(CallCredentials.createEmpty())
),
false
);
assert.strictEqual(
isEmptyCallCredentials(callCredentials1.compose(callCredentials2)),
false
);
});
});

describe('compose', () => {
it('should accept a CallCredentials object and return a new object', () => {
const callCredentials1 = CallCredentials.createFromMetadataGenerator(
Expand All @@ -65,6 +102,44 @@ describe('CallCredentials', () => {
assert.notStrictEqual(combinedCredentials, callCredentials2);
});

it('should return the same object when composed with empty credentials', async () => {
const callCredentials1 = CallCredentials.createFromMetadataGenerator(
generateFromServiceURL
);
const callCredentials2 = CallCredentials.createFromMetadataGenerator(
generateFromServiceURL
);
const emptyCredentials = CallCredentials.createEmpty();
assert.strictEqual(
emptyCredentials.compose(emptyCredentials),
emptyCredentials
);
assert.strictEqual(
callCredentials1.compose(emptyCredentials),
callCredentials1
);
assert.strictEqual(
emptyCredentials.compose(callCredentials1),
callCredentials1
);
const combinedCredentials = callCredentials1.compose(callCredentials2);
assert.strictEqual(
combinedCredentials.compose(emptyCredentials),
combinedCredentials
);
assert.strictEqual(
emptyCredentials.compose(combinedCredentials),
combinedCredentials
);
const metadata = await callCredentials1
.compose(emptyCredentials)
.generateMetadata({
method_name: 'bar',
service_url: 'foo',
});
assert.deepStrictEqual(metadata.get('service_url'), ['foo']);
});

it('should be chainable', () => {
const callCredentials1 = CallCredentials.createFromMetadataGenerator(
generateFromServiceURL
Expand Down Expand Up @@ -149,4 +224,111 @@ describe('CallCredentials', () => {
);
});
});

describe('LoadBalancingCall integration', () => {
function runLoadBalancingCallPick(
callCredentials: CallCredentials,
subchannelCredentials: CallCredentials,
initialMetadata: Metadata
): Promise<Metadata> {
return new Promise<Metadata>((resolve, reject) => {
const mockSubchannel: any = {
getCallCredentials: () => subchannelCredentials,
getConnectivityState: () => ConnectivityState.READY,
getRealSubchannel: () => mockSubchannel,
getChannelzRef: () => ({ id: 1 }),
getAddress: () => 'localhost:12345',
createCall: (metadata: Metadata) => {
resolve(metadata);
return {
getCallNumber: () => 1,
startRead: () => {},
sendMessageWithContext: () => {},
halfClose: () => {},
cancelWithStatus: () => {},
};
},
};
const mockChannel: any = {
getServiceUrl: (host: string, methodName: string) =>
computeServiceUrl(host, methodName),
doPick: () => ({
pickResultType: PickResultType.COMPLETE,
subchannel: mockSubchannel,
status: null,
onCallStarted: null,
onCallEnded: null,
}),
};
const callConfig: any = {
methodConfig: { name: [] },
pickInformation: {},
status: Status.OK,
dynamicFilterFactories: [],
};
const call = new LoadBalancingCall(
mockChannel,
callConfig,
'/service/method',
'localhost:12345',
callCredentials,
Infinity,
1
);
call.start(initialMetadata, {
onReceiveMetadata: () => {},
onReceiveMessage: () => {},
onReceiveStatus: status => {
reject(new Error(`Unexpected status: ${status.details}`));
},
});
});
}

it('should bypass generateMetadata when credentials are empty', async () => {
const emptyPrototype = Object.getPrototypeOf(
CallCredentials.createEmpty()
);
const originalGenerateMetadata = emptyPrototype.generateMetadata;
let generateMetadataCalls = 0;
emptyPrototype.generateMetadata = function (
...args: Parameters<CallCredentials['generateMetadata']>
) {
generateMetadataCalls += 1;
return originalGenerateMetadata.apply(this, args);
};
try {
const initialMetadata = new Metadata();
initialMetadata.set('custom-key', 'custom-value');
const finalMetadata = await runLoadBalancingCallPick(
CallCredentials.createEmpty(),
CallCredentials.createEmpty(),
initialMetadata
);
assert.strictEqual(generateMetadataCalls, 0);
assert.deepStrictEqual(finalMetadata.get('custom-key'), [
'custom-value',
]);
} finally {
emptyPrototype.generateMetadata = originalGenerateMetadata;
}
});

it('should call generateMetadata and merge metadata when credentials are non-empty', async () => {
const initialMetadata = new Metadata();
initialMetadata.set('custom-key', 'custom-value');
const callCredentials = CallCredentials.createFromMetadataGenerator(
generateFromServiceURL
);
const finalMetadata = await runLoadBalancingCallPick(
callCredentials,
CallCredentials.createEmpty(),
initialMetadata
);
assert.deepStrictEqual(finalMetadata.get('custom-key'), ['custom-value']);
assert.deepStrictEqual(finalMetadata.get('service_url'), [
'https://localhost/service',
]);
});
});
});
29 changes: 29 additions & 0 deletions packages/grpc-js/test/test-channel-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,35 @@ describe('ChannelCredentials Implementation', () => {
const composedChannelCreds = channelCreds.compose(callCreds);
assert.ok(composedChannelCreds instanceof ChannelCredentials);
});

it('should preserve unwrapped CallCredentials when creating a connector', () => {
const channelCreds = ChannelCredentials.createSsl();
const callCreds = CallCredentials.createFromMetadataGenerator(
(options, cb) => cb(null, new grpc.Metadata())
);
const composedChannelCreds = channelCreds.compose(callCreds);
const connector = composedChannelCreds._createSecureConnector(
{ scheme: 'dns', path: 'localhost' },
{}
);
assert.strictEqual(connector.getCallCredentials(), callCreds);
connector.destroy();
});
});

describe('createInsecure', () => {
it('should return the same default CallCredentials instance from a connector', () => {
const insecureCreds = ChannelCredentials.createInsecure();
const connector = insecureCreds._createSecureConnector(
{ scheme: 'dns', path: 'localhost' },
{}
);
assert.strictEqual(
connector.getCallCredentials(),
connector.getCallCredentials()
);
connector.destroy();
});
});
});

Expand Down
Loading