grpc-node/packages/grpc-js/test/test-resolver.ts

703 lines
26 KiB
TypeScript

/*
* Copyright 2019 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.
*
*/
// Allow `any` data type for testing runtime type checking.
// tslint:disable no-any
import * as assert from 'assert';
import * as resolverManager from '../src/resolver';
import * as resolver_dns from '../src/resolver-dns';
import * as resolver_uds from '../src/resolver-uds';
import * as resolver_ip from '../src/resolver-ip';
import { ServiceConfig } from '../src/service-config';
import { StatusObject } from '../src/call-interface';
import {
Endpoint,
SubchannelAddress,
endpointToString,
subchannelAddressEqual,
} from '../src/subchannel-address';
import { parseUri, GrpcUri } from '../src/uri-parser';
import { GRPC_NODE_USE_ALTERNATIVE_RESOLVER } from '../src/environment';
function hasMatchingAddress(
endpointList: Endpoint[],
expectedAddress: SubchannelAddress
): boolean {
for (const endpoint of endpointList) {
for (const address of endpoint.addresses) {
if (subchannelAddressEqual(address, expectedAddress)) {
return true;
}
}
}
return false;
}
describe('Name Resolver', () => {
before(() => {
resolver_dns.setup();
resolver_uds.setup();
resolver_ip.setup();
});
describe('DNS Names', function () {
// For some reason DNS queries sometimes take a long time on Windows
this.timeout(4000);
it('Should resolve localhost properly', function (done) {
if (GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
this.skip();
}
const target = resolverManager.mapUriDefaultScheme(
parseUri('localhost:50051')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 50051 })
);
assert(
hasMatchingAddress(endpointList, { host: '::1', port: 50051 })
);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('Should default to port 443', function (done) {
if (GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
this.skip();
}
const target = resolverManager.mapUriDefaultScheme(
parseUri('localhost')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 443 })
);
assert(hasMatchingAddress(endpointList, { host: '::1', port: 443 }));
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('Should correctly represent an ipv4 address', done => {
const target = resolverManager.mapUriDefaultScheme(parseUri('1.2.3.4')!)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
hasMatchingAddress(endpointList, { host: '1.2.3.4', port: 443 })
);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('Should correctly represent an ipv6 address', done => {
const target = resolverManager.mapUriDefaultScheme(parseUri('::1')!)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(hasMatchingAddress(endpointList, { host: '::1', port: 443 }));
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('Should correctly represent a bracketed ipv6 address', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('[::1]:50051')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
hasMatchingAddress(endpointList, { host: '::1', port: 50051 })
);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('Should resolve a public address', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('example.com')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(endpointList.length > 0);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
// Created DNS TXT record using TXT sample from https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
// "grpc_config=[{\"serviceConfig\":{\"loadBalancingPolicy\":\"round_robin\",\"methodConfig\":[{\"name\":[{\"service\":\"MyService\",\"method\":\"Foo\"}],\"waitForReady\":true}]}}]"
it.skip('Should resolve a name with TXT service config', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('grpctest.kleinsch.com')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
if (serviceConfig !== null) {
assert(
serviceConfig.loadBalancingPolicy === 'round_robin',
'Should have found round robin LB policy'
);
done();
}
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it.skip('Should not resolve TXT service config if we disabled service config', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('grpctest.kleinsch.com')!
)!;
let count = 0;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
assert(
serviceConfig === null,
'Should not have found service config'
);
count++;
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {
'grpc.service_config_disable_resolution': 1,
});
resolver.updateResolution();
setTimeout(() => {
assert(count === 1, 'Should have only resolved once');
done();
}, 2_000);
});
/* The DNS entry for loopback4.unittest.grpc.io only has a single A record
* with the address 127.0.0.1, but the Mac DNS resolver appears to use
* NAT64 to create an IPv6 address in that case, so it instead returns
* 64:ff9b::7f00:1. Handling that kind of translation is outside of the
* scope of this test, so we are skipping it. The test primarily exists
* as a regression test for https://github.com/grpc/grpc-node/issues/1044,
* and the test 'Should resolve gRPC interop servers' tests the same thing.
*/
it.skip('Should resolve a name with multiple dots', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('loopback4.unittest.grpc.io')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 443 }),
`None of [${endpointList.map(addr =>
endpointToString(addr)
)}] matched '127.0.0.1:443'`
);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
/* TODO(murgatroid99): re-enable this test, once we can get the IPv6 result
* consistently */
it.skip('Should resolve a DNS name to an IPv6 address', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('loopback6.unittest.grpc.io')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(hasMatchingAddress(endpointList, { host: '::1', port: 443 }));
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
/* This DNS name resolves to only the IPv4 address on Windows, and only the
* IPv6 address on Mac. There is no result that we can consistently test
* for here. */
it.skip('Should resolve a DNS name to IPv4 and IPv6 addresses', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('loopback46.unittest.grpc.io')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 443 }),
`None of [${endpointList.map(addr =>
endpointToString(addr)
)}] matched '127.0.0.1:443'`
);
/* TODO(murgatroid99): check for IPv6 result, once we can get that
* consistently */
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('Should resolve a name with a hyphen', done => {
/* TODO(murgatroid99): Find or create a better domain name to test this with.
* This is just the first one I found with a hyphen. */
const target = resolverManager.mapUriDefaultScheme(
parseUri('network-tools.com')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(endpointList.length > 0);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
/* This test also serves as a regression test for
* https://github.com/grpc/grpc-node/issues/1044, specifically handling
* hyphens and multiple periods in a DNS name. It should not be skipped
* unless there is another test for the same issue. */
it('Should resolve gRPC interop servers', done => {
let completeCount = 0;
const target1 = resolverManager.mapUriDefaultScheme(
parseUri('grpc-test.sandbox.googleapis.com')!
)!;
const target2 = resolverManager.mapUriDefaultScheme(
parseUri('grpc-test4.sandbox.googleapis.com')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
assert(endpointList.length > 0);
completeCount += 1;
if (completeCount === 2) {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
done();
}
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver1 = resolverManager.createResolver(target1, listener, {});
resolver1.updateResolution();
const resolver2 = resolverManager.createResolver(target2, listener, {});
resolver2.updateResolution();
});
it('should not keep repeating successful resolutions', function (done) {
if (GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
this.skip();
}
const target = resolverManager.mapUriDefaultScheme(
parseUri('localhost')!
)!;
let resultCount = 0;
const resolver = resolverManager.createResolver(
target,
{
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
assert(
hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 443 })
);
assert(
hasMatchingAddress(endpointList, { host: '::1', port: 443 })
);
resultCount += 1;
if (resultCount === 1) {
process.nextTick(() => resolver.updateResolution());
}
},
onError: (error: StatusObject) => {
assert.ifError(error);
},
},
{ 'grpc.dns_min_time_between_resolutions_ms': 2000 }
);
resolver.updateResolution();
setTimeout(() => {
assert.strictEqual(resultCount, 2, `resultCount ${resultCount} !== 2`);
done();
}, 10_000);
}).timeout(15_000);
it('should not keep repeating failed resolutions', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('host.invalid')!
)!;
let resultCount = 0;
const resolver = resolverManager.createResolver(
target,
{
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
assert.fail('Resolution succeeded unexpectedly');
},
onError: (error: StatusObject) => {
resultCount += 1;
if (resultCount === 1) {
process.nextTick(() => resolver.updateResolution());
}
},
},
{}
);
resolver.updateResolution();
setTimeout(() => {
assert.strictEqual(resultCount, 2, `resultCount ${resultCount} !== 2`);
done();
}, 10_000);
}).timeout(15_000);
});
describe('UDS Names', () => {
it('Should handle a relative Unix Domain Socket name', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('unix:socket')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(hasMatchingAddress(endpointList, { path: 'socket' }));
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('Should handle an absolute Unix Domain Socket name', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('unix:///tmp/socket')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(hasMatchingAddress(endpointList, { path: '/tmp/socket' }));
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
});
describe('IP Addresses', () => {
it('should handle one IPv4 address with no port', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('ipv4:127.0.0.1')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 443 })
);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('should handle one IPv4 address with a port', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('ipv4:127.0.0.1:50051')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 50051 })
);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('should handle multiple IPv4 addresses with different ports', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('ipv4:127.0.0.1:50051,127.0.0.1:50052')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 50051 })
);
assert(
hasMatchingAddress(endpointList, { host: '127.0.0.1', port: 50052 })
);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('should handle one IPv6 address with no port', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('ipv6:::1')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(hasMatchingAddress(endpointList, { host: '::1', port: 443 }));
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('should handle one IPv6 address with a port', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('ipv6:[::1]:50051')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
hasMatchingAddress(endpointList, { host: '::1', port: 50051 })
);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
it('should handle multiple IPv6 addresses with different ports', done => {
const target = resolverManager.mapUriDefaultScheme(
parseUri('ipv6:[::1]:50051,[::1]:50052')!
)!;
const listener: resolverManager.ResolverListener = {
onSuccessfulResolution: (
endpointList: Endpoint[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null
) => {
// Only handle the first resolution result
listener.onSuccessfulResolution = () => {};
assert(
hasMatchingAddress(endpointList, { host: '::1', port: 50051 })
);
assert(
hasMatchingAddress(endpointList, { host: '::1', port: 50052 })
);
done();
},
onError: (error: StatusObject) => {
done(new Error(`Failed with status ${error.details}`));
},
};
const resolver = resolverManager.createResolver(target, listener, {});
resolver.updateResolution();
});
});
describe('getDefaultAuthority', () => {
class OtherResolver implements resolverManager.Resolver {
updateResolution() {
return [];
}
destroy() {}
static getDefaultAuthority(target: GrpcUri): string {
return 'other';
}
}
it('Should return the correct authority if a different resolver has been registered', () => {
resolverManager.registerResolver('other', OtherResolver);
const target = resolverManager.mapUriDefaultScheme(
parseUri('other:name')!
)!;
console.log(target);
const authority = resolverManager.getDefaultAuthority(target);
assert.equal(authority, 'other');
});
});
});