@globalworldwide/grpc-resolvers
Version:
Custom resolvers for @grpc/grpc-js
166 lines • 6.46 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.addListener = addListener;
exports.removeListener = removeListener;
exports.getEndpoints = getEndpoints;
const tslib_1 = require("tslib");
const k8s = tslib_1.__importStar(require("@kubernetes/client-node"));
const lodash_shuffle_1 = tslib_1.__importDefault(require("lodash.shuffle"));
const logging_js_1 = require("./logging.js");
const logger = (0, logging_js_1.makeLogger)('k8s-watch');
let watch;
let watchRequest;
const listenersByServiceName = new Map();
const endpointsByEndpointName = new Map();
const grpcEndpointByServiceName = new Map();
function addListener(serviceName, listener) {
logger.debug?.(`watch addListener ${serviceName}`);
const listeners = listenersByServiceName.get(serviceName) ?? new Set();
listenersByServiceName.set(serviceName, listeners);
listeners.add(listener);
void startWatch();
}
function removeListener(serviceName, listener) {
logger.debug?.(`watch removeListener ${serviceName}`);
const listeners = listenersByServiceName.get(serviceName) ?? new Set();
listenersByServiceName.set(serviceName, listeners);
listeners.delete(listener);
if (listeners.size === 0) {
listenersByServiceName.delete(serviceName);
// MSED do we want to stop here?
stopWatch();
}
}
function getEndpoints(serviceName) {
return grpcEndpointByServiceName.get(serviceName) ?? [];
}
function hasListeners() {
return listenersByServiceName.size > 0;
}
function normalizeEndpointSlice(endpointSlice) {
logger.debug?.('endpoint slice', endpointSlice);
const endpointName = endpointSlice.metadata?.name;
if (!endpointName) {
return undefined;
}
return {
endpointName,
serviceName: endpointSlice.metadata?.labels?.['kubernetes.io/service-name'] ?? '',
hosts: (0, lodash_shuffle_1.default)((endpointSlice.endpoints ?? [])
.filter((endpoint) => endpoint.conditions?.ready &&
endpoint.conditions.serving &&
!endpoint.conditions.terminating)
.flatMap((endpoint) => endpoint.addresses)),
ports: (endpointSlice.ports ?? [])
.filter((p) => p.name && p.name.length > 0 && p.protocol === 'TCP' && p.port !== undefined)
.map((p) => ({ portName: p.name, port: p.port })),
};
}
async function startWatch() {
try {
if (watch !== undefined || !hasListeners()) {
return;
}
// load kubeconfig from default location and determine namespace
const kc = new k8s.KubeConfig();
kc.loadFromDefault();
const namespace = kc.contexts[0]?.namespace;
if (!namespace) {
throw new Error('No namespace found in KubeConfig');
}
// get an initial list of endpoint slices
const discoveryApi = kc.makeApiClient(k8s.DiscoveryV1Api);
const endpointSlices = await discoveryApi.listNamespacedEndpointSlice({ namespace });
// save a copy of the old endpoint map
const oldEndpoints = new Map(endpointsByEndpointName);
// build the new endpoint map
const newEndpoints = endpointSlices.items
.map(normalizeEndpointSlice)
.filter((e) => !!e);
endpointsByEndpointName.clear();
for (const newEndpoint of newEndpoints) {
endpointsByEndpointName.set(newEndpoint.endpointName, newEndpoint);
// remove the endpoint from the old endpoint map
oldEndpoints.delete(newEndpoint.endpointName);
}
// find the list of all the service names that have changed
const changedServiceNames = new Set();
for (const endpoint of oldEndpoints.values()) {
changedServiceNames.add(endpoint.serviceName);
}
for (const endpoint of newEndpoints.values()) {
changedServiceNames.add(endpoint.serviceName);
}
// notify the listeners
for (const changedServiceName of changedServiceNames) {
notifyListeners(changedServiceName);
}
// no start the watch, picking up from where the initial list left off
watch = new k8s.Watch(kc);
watchRequest = await watch.watch(`/apis/discovery.k8s.io/v1/namespaces/${namespace}/endpointslices`, { resourceVersion: endpointSlices.metadata?.resourceVersion }, (type, apiObj) => {
// convert to endpoints
const endpoint = normalizeEndpointSlice(apiObj);
if (!endpoint) {
return;
}
// update the endpoint map
if (type === 'DELETED') {
endpointsByEndpointName.delete(endpoint.endpointName);
}
else if (type === 'ADDED' || type === 'MODIFIED') {
endpointsByEndpointName.set(endpoint.endpointName, endpoint);
}
// notify the listeners
notifyListeners(endpoint.serviceName);
}, (e) => {
logger.errorError?.(e, 'watch terminated');
backoffWatch();
});
}
catch (e) {
// swallow all errors in the watch
logger.errorError?.(e, 'watch start error');
backoffWatch();
}
}
function backoffWatch() {
// MSED - add proper backoff logic on restarting the watch
stopWatch();
setTimeout(() => {
void startWatch();
}, 1000);
}
function stopWatch() {
try {
if (watchRequest) {
watchRequest.abort();
}
}
catch (e) {
// swallow errors attempting to stop the watch, nothing we can do about it
logger.errorError?.(e, 'watch stop error');
}
watch = undefined;
watchRequest = undefined;
}
function rebuildGrpcEndpoints(serviceName) {
grpcEndpointByServiceName.set(serviceName, Array.from(endpointsByEndpointName.values())
.filter((e) => e.serviceName === serviceName)
.flatMap((e) => {
return e.hosts.flatMap((host) => {
return e.ports.map((port) => ({ addresses: [{ host, ...port }] }));
});
}));
}
function notifyListeners(serviceName) {
rebuildGrpcEndpoints(serviceName);
const listeners = listenersByServiceName.get(serviceName);
if (!listeners || listeners.size === 0) {
return;
}
for (const listener of listeners) {
logger.debug?.(`watch notifyOne ${serviceName}`);
listener();
}
}
//# sourceMappingURL=k8s-watch.js.map