UNPKG

@globalworldwide/grpc-resolvers

Version:
166 lines 6.46 kB
"use strict"; 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