@digifi/k8s-grpc-js-resolver 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +14 -0
- package/README.md +98 -0
- package/dist/dns-fallback.d.ts +20 -0
- package/dist/dns-fallback.d.ts.map +1 -0
- package/dist/dns-fallback.js +84 -0
- package/dist/dns-fallback.js.map +1 -0
- package/dist/endpoint-slice-store.d.ts +81 -0
- package/dist/endpoint-slice-store.d.ts.map +1 -0
- package/dist/endpoint-slice-store.js +201 -0
- package/dist/endpoint-slice-store.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/k8s-api-client.d.ts +18 -0
- package/dist/k8s-api-client.d.ts.map +1 -0
- package/dist/k8s-api-client.js +125 -0
- package/dist/k8s-api-client.js.map +1 -0
- package/dist/k8s-endpoint-watch.d.ts +4 -0
- package/dist/k8s-endpoint-watch.d.ts.map +1 -0
- package/dist/k8s-endpoint-watch.js +194 -0
- package/dist/k8s-endpoint-watch.js.map +1 -0
- package/dist/k8s-resolver.d.ts +43 -0
- package/dist/k8s-resolver.d.ts.map +1 -0
- package/dist/k8s-resolver.js +230 -0
- package/dist/k8s-resolver.js.map +1 -0
- package/dist/resolve-grpc-target-address.d.ts +8 -0
- package/dist/resolve-grpc-target-address.d.ts.map +1 -0
- package/dist/resolve-grpc-target-address.js +34 -0
- package/dist/resolve-grpc-target-address.js.map +1 -0
- package/dist/resolver-logger.d.ts +13 -0
- package/dist/resolver-logger.d.ts.map +1 -0
- package/dist/resolver-logger.js +41 -0
- package/dist/resolver-logger.js.map +1 -0
- package/dist/resolver.constants.d.ts +7 -0
- package/dist/resolver.constants.d.ts.map +1 -0
- package/dist/resolver.constants.js +17 -0
- package/dist/resolver.constants.js.map +1 -0
- package/dist/rewrite-grpc-address.d.ts +32 -0
- package/dist/rewrite-grpc-address.d.ts.map +1 -0
- package/dist/rewrite-grpc-address.js +96 -0
- package/dist/rewrite-grpc-address.js.map +1 -0
- package/dist/watch-stream-parser.d.ts +23 -0
- package/dist/watch-stream-parser.d.ts.map +1 -0
- package/dist/watch-stream-parser.js +51 -0
- package/dist/watch-stream-parser.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveGrpcTargetAddress = exports.registerK8sResolver = void 0;
|
|
4
|
+
const grpc_js_1 = require("@grpc/grpc-js");
|
|
5
|
+
const k8s_resolver_1 = require("./k8s-resolver");
|
|
6
|
+
const resolver_constants_1 = require("./resolver.constants");
|
|
7
|
+
const rewrite_grpc_address_1 = require("./rewrite-grpc-address");
|
|
8
|
+
let registered = false;
|
|
9
|
+
/** Registers the `k8s://` resolver with the process-wide grpc-js resolver registry. */
|
|
10
|
+
const registerK8sResolver = () => {
|
|
11
|
+
if (registered) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
grpc_js_1.experimental.registerResolver(resolver_constants_1.K8S_RESOLVER_SCHEME, k8s_resolver_1.K8sResolver);
|
|
15
|
+
registered = true;
|
|
16
|
+
};
|
|
17
|
+
exports.registerK8sResolver = registerK8sResolver;
|
|
18
|
+
/**
|
|
19
|
+
* Turns an in-cluster `host:port` address into a Kubernetes-backed gRPC target when
|
|
20
|
+
* `GRPC_K8S_RESOLVER_ENABLED` is enabled. Explicit `k8s://` targets work without the flag.
|
|
21
|
+
*/
|
|
22
|
+
const resolveGrpcTargetAddress = (address) => {
|
|
23
|
+
if (address.startsWith(`${resolver_constants_1.K8S_RESOLVER_SCHEME}://`)) {
|
|
24
|
+
(0, exports.registerK8sResolver)();
|
|
25
|
+
return address;
|
|
26
|
+
}
|
|
27
|
+
if (!(0, rewrite_grpc_address_1.isK8sResolverEnabled)()) {
|
|
28
|
+
return address;
|
|
29
|
+
}
|
|
30
|
+
(0, exports.registerK8sResolver)();
|
|
31
|
+
return (0, rewrite_grpc_address_1.rewriteGrpcAddressIfEnabled)(address);
|
|
32
|
+
};
|
|
33
|
+
exports.resolveGrpcTargetAddress = resolveGrpcTargetAddress;
|
|
34
|
+
//# sourceMappingURL=resolve-grpc-target-address.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-grpc-target-address.js","sourceRoot":"","sources":["../src/resolve-grpc-target-address.ts"],"names":[],"mappings":";;;AAAA,2CAA6C;AAC7C,iDAA6C;AAC7C,6DAA2D;AAC3D,iEAA2F;AAE3F,IAAI,UAAU,GAAG,KAAK,CAAC;AAEvB,uFAAuF;AAChF,MAAM,mBAAmB,GAAG,GAAS,EAAE;IAC5C,IAAI,UAAU,EAAE,CAAC;QACf,OAAO;IACT,CAAC;IAED,sBAAY,CAAC,gBAAgB,CAAC,wCAAmB,EAAE,0BAAW,CAAC,CAAC;IAChE,UAAU,GAAG,IAAI,CAAC;AACpB,CAAC,CAAC;AAPW,QAAA,mBAAmB,uBAO9B;AAEF;;;GAGG;AACI,MAAM,wBAAwB,GAAG,CAAC,OAAe,EAAU,EAAE;IAClE,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,wCAAmB,KAAK,CAAC,EAAE,CAAC;QACpD,IAAA,2BAAmB,GAAE,CAAC;QAEtB,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,IAAI,CAAC,IAAA,2CAAoB,GAAE,EAAE,CAAC;QAC5B,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,IAAA,2BAAmB,GAAE,CAAC;IAEtB,OAAO,IAAA,kDAA2B,EAAC,OAAO,CAAC,CAAC;AAC9C,CAAC,CAAC;AAdW,QAAA,wBAAwB,4BAcnC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface IResolverLogger {
|
|
2
|
+
info(message: string): void;
|
|
3
|
+
warn(message: string): void;
|
|
4
|
+
error(message: string): void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The resolver runs before (and outside of) the Nest DI container, so it owns a standalone logger
|
|
8
|
+
* the same way the application bootstrap and CLI entry points do.
|
|
9
|
+
*/
|
|
10
|
+
export declare const getResolverLogger: () => IResolverLogger;
|
|
11
|
+
export declare const toErrorMessage: (error: unknown) => string;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=resolver-logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver-logger.d.ts","sourceRoot":"","sources":["../src/resolver-logger.ts"],"names":[],"mappings":"AAYA,UAAU,eAAe;IACvB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAoBD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QAAO,eAAyB,CAAC;AAE/D,eAAO,MAAM,cAAc,GAAI,OAAO,OAAO,KAAG,MAM/C,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toErrorMessage = exports.getResolverLogger = void 0;
|
|
4
|
+
const LOG_LEVEL_ENV = 'LogLevel';
|
|
5
|
+
const DEFAULT_LOG_LEVEL = 'info';
|
|
6
|
+
/** Mirrors the level ordering WinstonLogger is configured with. */
|
|
7
|
+
const LOG_LEVEL_PRIORITY = {
|
|
8
|
+
error: 0,
|
|
9
|
+
warn: 1,
|
|
10
|
+
info: 2,
|
|
11
|
+
verbose: 3,
|
|
12
|
+
debug: 4,
|
|
13
|
+
trace: 5,
|
|
14
|
+
};
|
|
15
|
+
const write = (level, message) => {
|
|
16
|
+
const configuredLevel = process.env[LOG_LEVEL_ENV] ?? DEFAULT_LOG_LEVEL;
|
|
17
|
+
const configuredPriority = LOG_LEVEL_PRIORITY[configuredLevel] ?? LOG_LEVEL_PRIORITY[DEFAULT_LOG_LEVEL];
|
|
18
|
+
if (LOG_LEVEL_PRIORITY[level] > configuredPriority) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
process.stdout.write(`${new Date().toISOString()} ${level}: ${message}\n`);
|
|
22
|
+
};
|
|
23
|
+
const logger = {
|
|
24
|
+
info: (message) => write('info', message),
|
|
25
|
+
warn: (message) => write('warn', message),
|
|
26
|
+
error: (message) => write('error', message),
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The resolver runs before (and outside of) the Nest DI container, so it owns a standalone logger
|
|
30
|
+
* the same way the application bootstrap and CLI entry points do.
|
|
31
|
+
*/
|
|
32
|
+
const getResolverLogger = () => logger;
|
|
33
|
+
exports.getResolverLogger = getResolverLogger;
|
|
34
|
+
const toErrorMessage = (error) => {
|
|
35
|
+
if (error instanceof Error) {
|
|
36
|
+
return error.message;
|
|
37
|
+
}
|
|
38
|
+
return String(error);
|
|
39
|
+
};
|
|
40
|
+
exports.toErrorMessage = toErrorMessage;
|
|
41
|
+
//# sourceMappingURL=resolver-logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver-logger.js","sourceRoot":"","sources":["../src/resolver-logger.ts"],"names":[],"mappings":";;;AAAA,MAAM,aAAa,GAAG,UAAU,CAAC;AACjC,MAAM,iBAAiB,GAAG,MAAM,CAAC;AACjC,mEAAmE;AACnE,MAAM,kBAAkB,GAA2B;IACjD,KAAK,EAAE,CAAC;IACR,IAAI,EAAE,CAAC;IACP,IAAI,EAAE,CAAC;IACP,OAAO,EAAE,CAAC;IACV,KAAK,EAAE,CAAC;IACR,KAAK,EAAE,CAAC;CACT,CAAC;AAQF,MAAM,KAAK,GAAG,CAAC,KAAgC,EAAE,OAAe,EAAQ,EAAE;IACxE,MAAM,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,iBAAiB,CAAC;IACxE,MAAM,kBAAkB,GACtB,kBAAkB,CAAC,eAAe,CAAC,IAAI,kBAAkB,CAAC,iBAAiB,CAAC,CAAC;IAE/E,IAAI,kBAAkB,CAAC,KAAK,CAAC,GAAG,kBAAkB,EAAE,CAAC;QACnD,OAAO;IACT,CAAC;IAED,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,KAAK,KAAK,OAAO,IAAI,CAAC,CAAC;AAC7E,CAAC,CAAC;AAEF,MAAM,MAAM,GAAoB;IAC9B,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC;IACzC,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC;IACzC,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC;CAC5C,CAAC;AAEF;;;GAGG;AACI,MAAM,iBAAiB,GAAG,GAAoB,EAAE,CAAC,MAAM,CAAC;AAAlD,QAAA,iBAAiB,qBAAiC;AAExD,MAAM,cAAc,GAAG,CAAC,KAAc,EAAU,EAAE;IACvD,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC,OAAO,CAAC;IACvB,CAAC;IAED,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC,CAAC;AANW,QAAA,cAAc,kBAMzB"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Kept separate from the resolver and rewriting modules so neither has to import the other. */
|
|
2
|
+
export declare const K8S_RESOLVER_SCHEME = "k8s";
|
|
3
|
+
export declare const GRPC_K8S_RESOLVER_ENABLED_ENV = "GRPC_K8S_RESOLVER_ENABLED";
|
|
4
|
+
/** When true, fall back to headless service DNS if EndpointSlice discovery fails or returns no ready pods. */
|
|
5
|
+
export declare const GRPC_K8S_RESOLVER_DNS_FALLBACK_ENV = "GRPC_K8S_RESOLVER_DNS_FALLBACK";
|
|
6
|
+
export declare const isDnsFallbackEnabled: () => boolean;
|
|
7
|
+
//# sourceMappingURL=resolver.constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.constants.d.ts","sourceRoot":"","sources":["../src/resolver.constants.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAEzC,eAAO,MAAM,6BAA6B,8BAA8B,CAAC;AAEzE,8GAA8G;AAC9G,eAAO,MAAM,kCAAkC,mCAAmC,CAAC;AAEnF,eAAO,MAAM,oBAAoB,QAAO,OAQvC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isDnsFallbackEnabled = exports.GRPC_K8S_RESOLVER_DNS_FALLBACK_ENV = exports.GRPC_K8S_RESOLVER_ENABLED_ENV = exports.K8S_RESOLVER_SCHEME = void 0;
|
|
4
|
+
/** Kept separate from the resolver and rewriting modules so neither has to import the other. */
|
|
5
|
+
exports.K8S_RESOLVER_SCHEME = 'k8s';
|
|
6
|
+
exports.GRPC_K8S_RESOLVER_ENABLED_ENV = 'GRPC_K8S_RESOLVER_ENABLED';
|
|
7
|
+
/** When true, fall back to headless service DNS if EndpointSlice discovery fails or returns no ready pods. */
|
|
8
|
+
exports.GRPC_K8S_RESOLVER_DNS_FALLBACK_ENV = 'GRPC_K8S_RESOLVER_DNS_FALLBACK';
|
|
9
|
+
const isDnsFallbackEnabled = () => {
|
|
10
|
+
const value = process.env[exports.GRPC_K8S_RESOLVER_DNS_FALLBACK_ENV];
|
|
11
|
+
if (value === undefined) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
return value === 'true' || value === '1';
|
|
15
|
+
};
|
|
16
|
+
exports.isDnsFallbackEnabled = isDnsFallbackEnabled;
|
|
17
|
+
//# sourceMappingURL=resolver.constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.constants.js","sourceRoot":"","sources":["../src/resolver.constants.ts"],"names":[],"mappings":";;;AAAA,gGAAgG;AACnF,QAAA,mBAAmB,GAAG,KAAK,CAAC;AAE5B,QAAA,6BAA6B,GAAG,2BAA2B,CAAC;AAEzE,8GAA8G;AACjG,QAAA,kCAAkC,GAAG,gCAAgC,CAAC;AAE5E,MAAM,oBAAoB,GAAG,GAAY,EAAE;IAChD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,0CAAkC,CAAC,CAAC;IAE9D,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,GAAG,CAAC;AAC3C,CAAC,CAAC;AARW,QAAA,oBAAoB,wBAQ/B"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface IHostPort {
|
|
2
|
+
host: string;
|
|
3
|
+
port: string;
|
|
4
|
+
}
|
|
5
|
+
export interface IServiceCoordinates {
|
|
6
|
+
serviceName: string;
|
|
7
|
+
namespace: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const isK8sResolverEnabled: () => boolean;
|
|
10
|
+
export declare const readPodNamespace: () => string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Parses the plain `host:port` form the gRPC URL secrets store today. Anything carrying a scheme,
|
|
13
|
+
* an IPv6 literal or extra colons is rejected outright rather than guessed at.
|
|
14
|
+
*/
|
|
15
|
+
export declare const parseHostPortAddress: (address: string) => IHostPort | null;
|
|
16
|
+
/**
|
|
17
|
+
* Recognises only the in-cluster service DNS forms, so external hostnames, IP literals and
|
|
18
|
+
* loopback addresses keep flowing to the stock DNS resolver instead of being rewritten into a
|
|
19
|
+
* `k8s://` target that could never resolve. Accepted:
|
|
20
|
+
* - `<service>` (namespace taken from the pod)
|
|
21
|
+
* - `<service>.<namespace>` (only when the namespace is the pod's own)
|
|
22
|
+
* - `<service>.<namespace>.svc[.<cluster-domain>]`
|
|
23
|
+
*/
|
|
24
|
+
export declare const extractServiceAndNamespace: (host: string, podNamespace: string | null) => IServiceCoordinates | null;
|
|
25
|
+
export declare const toK8sResolverAddress: (serviceName: string, namespace: string, port: string) => string;
|
|
26
|
+
/**
|
|
27
|
+
* When `GRPC_K8S_RESOLVER_ENABLED` is set, rewrite in-cluster service DNS `host:port` to
|
|
28
|
+
* `k8s://<namespace>/<service>:<port>`. Any address that is not recognisably an in-cluster
|
|
29
|
+
* service is returned untouched.
|
|
30
|
+
*/
|
|
31
|
+
export declare const rewriteGrpcAddressIfEnabled: (address: string) => string;
|
|
32
|
+
//# sourceMappingURL=rewrite-grpc-address.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rewrite-grpc-address.d.ts","sourceRoot":"","sources":["../src/rewrite-grpc-address.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,oBAAoB,QAAO,OAIvC,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAO,MAAM,GAAG,IAE5C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,KAAG,SAAS,GAAG,IAmBlE,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,GACrC,MAAM,MAAM,EACZ,cAAc,MAAM,GAAG,IAAI,KAC1B,mBAAmB,GAAG,IA6BxB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,aAAa,MAAM,EACnB,WAAW,MAAM,EACjB,MAAM,MAAM,KACX,MAAwE,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,GAAI,SAAS,MAAM,KAAG,MAkB7D,CAAC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rewriteGrpcAddressIfEnabled = exports.toK8sResolverAddress = exports.extractServiceAndNamespace = exports.parseHostPortAddress = exports.readPodNamespace = exports.isK8sResolverEnabled = void 0;
|
|
4
|
+
const resolver_constants_1 = require("./resolver.constants");
|
|
5
|
+
const k8s_api_client_1 = require("./k8s-api-client");
|
|
6
|
+
const DNS_LABEL_PATTERN = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/;
|
|
7
|
+
const IPV4_PATTERN = /^\d{1,3}(\.\d{1,3}){3}$/;
|
|
8
|
+
const NUMERIC_PATTERN = /^\d+$/;
|
|
9
|
+
const PORT_PATTERN = /^\d+$/;
|
|
10
|
+
const SERVICE_LABEL_MARKER = 'svc';
|
|
11
|
+
const LOOPBACK_HOSTS = new Set(['localhost', 'localhost.localdomain']);
|
|
12
|
+
const MIN_PORT = 1;
|
|
13
|
+
const MAX_PORT = 65_535;
|
|
14
|
+
const isK8sResolverEnabled = () => {
|
|
15
|
+
const value = process.env[resolver_constants_1.GRPC_K8S_RESOLVER_ENABLED_ENV];
|
|
16
|
+
return value === 'true' || value === '1';
|
|
17
|
+
};
|
|
18
|
+
exports.isK8sResolverEnabled = isK8sResolverEnabled;
|
|
19
|
+
const readPodNamespace = () => {
|
|
20
|
+
return process.env.POD_NAMESPACE || (0, k8s_api_client_1.readServiceAccountNamespace)();
|
|
21
|
+
};
|
|
22
|
+
exports.readPodNamespace = readPodNamespace;
|
|
23
|
+
/**
|
|
24
|
+
* Parses the plain `host:port` form the gRPC URL secrets store today. Anything carrying a scheme,
|
|
25
|
+
* an IPv6 literal or extra colons is rejected outright rather than guessed at.
|
|
26
|
+
*/
|
|
27
|
+
const parseHostPortAddress = (address) => {
|
|
28
|
+
if (!address || address.includes('://') || address.includes('[') || address.includes(']')) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const separatorIndex = address.indexOf(':');
|
|
32
|
+
if (separatorIndex <= 0 || separatorIndex !== address.lastIndexOf(':')) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const host = address.slice(0, separatorIndex);
|
|
36
|
+
const port = address.slice(separatorIndex + 1);
|
|
37
|
+
if (!PORT_PATTERN.test(port) || Number(port) < MIN_PORT || Number(port) > MAX_PORT) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return { host, port };
|
|
41
|
+
};
|
|
42
|
+
exports.parseHostPortAddress = parseHostPortAddress;
|
|
43
|
+
/**
|
|
44
|
+
* Recognises only the in-cluster service DNS forms, so external hostnames, IP literals and
|
|
45
|
+
* loopback addresses keep flowing to the stock DNS resolver instead of being rewritten into a
|
|
46
|
+
* `k8s://` target that could never resolve. Accepted:
|
|
47
|
+
* - `<service>` (namespace taken from the pod)
|
|
48
|
+
* - `<service>.<namespace>` (only when the namespace is the pod's own)
|
|
49
|
+
* - `<service>.<namespace>.svc[.<cluster-domain>]`
|
|
50
|
+
*/
|
|
51
|
+
const extractServiceAndNamespace = (host, podNamespace) => {
|
|
52
|
+
if (!host ||
|
|
53
|
+
host.includes('/') ||
|
|
54
|
+
LOOPBACK_HOSTS.has(host.toLowerCase()) ||
|
|
55
|
+
IPV4_PATTERN.test(host)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const labels = host.toLowerCase().split('.');
|
|
59
|
+
if (labels.some((label) => !DNS_LABEL_PATTERN.test(label)) || NUMERIC_PATTERN.test(labels[0])) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (labels.length === 1) {
|
|
63
|
+
return podNamespace ? { serviceName: labels[0], namespace: podNamespace } : null;
|
|
64
|
+
}
|
|
65
|
+
if (labels.length === 2) {
|
|
66
|
+
return labels[1] === podNamespace ? { serviceName: labels[0], namespace: labels[1] } : null;
|
|
67
|
+
}
|
|
68
|
+
if (labels[2] !== SERVICE_LABEL_MARKER) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return { serviceName: labels[0], namespace: labels[1] };
|
|
72
|
+
};
|
|
73
|
+
exports.extractServiceAndNamespace = extractServiceAndNamespace;
|
|
74
|
+
const toK8sResolverAddress = (serviceName, namespace, port) => `${resolver_constants_1.K8S_RESOLVER_SCHEME}://${namespace}/${serviceName}:${port}`;
|
|
75
|
+
exports.toK8sResolverAddress = toK8sResolverAddress;
|
|
76
|
+
/**
|
|
77
|
+
* When `GRPC_K8S_RESOLVER_ENABLED` is set, rewrite in-cluster service DNS `host:port` to
|
|
78
|
+
* `k8s://<namespace>/<service>:<port>`. Any address that is not recognisably an in-cluster
|
|
79
|
+
* service is returned untouched.
|
|
80
|
+
*/
|
|
81
|
+
const rewriteGrpcAddressIfEnabled = (address) => {
|
|
82
|
+
if (!(0, exports.isK8sResolverEnabled)() || address.startsWith(`${resolver_constants_1.K8S_RESOLVER_SCHEME}://`)) {
|
|
83
|
+
return address;
|
|
84
|
+
}
|
|
85
|
+
const hostPort = (0, exports.parseHostPortAddress)(address);
|
|
86
|
+
if (!hostPort) {
|
|
87
|
+
return address;
|
|
88
|
+
}
|
|
89
|
+
const coordinates = (0, exports.extractServiceAndNamespace)(hostPort.host, (0, exports.readPodNamespace)());
|
|
90
|
+
if (!coordinates) {
|
|
91
|
+
return address;
|
|
92
|
+
}
|
|
93
|
+
return (0, exports.toK8sResolverAddress)(coordinates.serviceName, coordinates.namespace, hostPort.port);
|
|
94
|
+
};
|
|
95
|
+
exports.rewriteGrpcAddressIfEnabled = rewriteGrpcAddressIfEnabled;
|
|
96
|
+
//# sourceMappingURL=rewrite-grpc-address.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rewrite-grpc-address.js","sourceRoot":"","sources":["../src/rewrite-grpc-address.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { IRawEndpointSlice } from './endpoint-slice-store';
|
|
2
|
+
export interface IWatchEvent {
|
|
3
|
+
type: string;
|
|
4
|
+
object: IRawEndpointSlice & {
|
|
5
|
+
reason?: string;
|
|
6
|
+
code?: number;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The Kubernetes watch endpoint streams newline-delimited JSON. Events are split across arbitrary
|
|
11
|
+
* chunk boundaries, so the trailing partial line has to be carried over to the next chunk.
|
|
12
|
+
*/
|
|
13
|
+
export declare class WatchStreamParser {
|
|
14
|
+
private buffer;
|
|
15
|
+
push(chunk: string): IWatchEvent[];
|
|
16
|
+
private parseLine;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* `410 Gone` means the resource version is too old to resume from and a full relist is required.
|
|
20
|
+
* The API server reports it either as an HTTP status or as an inline `ERROR` event.
|
|
21
|
+
*/
|
|
22
|
+
export declare const isExpiredResourceVersionEvent: (event: IWatchEvent) => boolean;
|
|
23
|
+
//# sourceMappingURL=watch-stream-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch-stream-parser.d.ts","sourceRoot":"","sources":["../src/watch-stream-parser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,iBAAiB,GAAG;QAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAED;;;GAGG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAM;IAEb,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,EAAE;IAoBzC,OAAO,CAAC,SAAS;CAkBlB;AAID;;;GAGG;AACH,eAAO,MAAM,6BAA6B,GAAI,OAAO,WAAW,KAAG,OAKlE,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isExpiredResourceVersionEvent = exports.WatchStreamParser = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The Kubernetes watch endpoint streams newline-delimited JSON. Events are split across arbitrary
|
|
6
|
+
* chunk boundaries, so the trailing partial line has to be carried over to the next chunk.
|
|
7
|
+
*/
|
|
8
|
+
class WatchStreamParser {
|
|
9
|
+
buffer = '';
|
|
10
|
+
push(chunk) {
|
|
11
|
+
this.buffer += chunk;
|
|
12
|
+
const lines = this.buffer.split('\n');
|
|
13
|
+
this.buffer = lines.pop() ?? '';
|
|
14
|
+
const events = [];
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const event = this.parseLine(line);
|
|
17
|
+
if (event) {
|
|
18
|
+
events.push(event);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return events;
|
|
22
|
+
}
|
|
23
|
+
parseLine(line) {
|
|
24
|
+
if (!line.trim()) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(line);
|
|
29
|
+
if (!parsed.type || !parsed.object) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return { type: parsed.type, object: parsed.object };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// A malformed line cannot be recovered from; the watch driver relists on stream failure.
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.WatchStreamParser = WatchStreamParser;
|
|
41
|
+
const EXPIRED_RESOURCE_VERSION_REASON = 'Expired';
|
|
42
|
+
/**
|
|
43
|
+
* `410 Gone` means the resource version is too old to resume from and a full relist is required.
|
|
44
|
+
* The API server reports it either as an HTTP status or as an inline `ERROR` event.
|
|
45
|
+
*/
|
|
46
|
+
const isExpiredResourceVersionEvent = (event) => {
|
|
47
|
+
return (event.type === 'ERROR' &&
|
|
48
|
+
(event.object.reason === EXPIRED_RESOURCE_VERSION_REASON || event.object.code === 410));
|
|
49
|
+
};
|
|
50
|
+
exports.isExpiredResourceVersionEvent = isExpiredResourceVersionEvent;
|
|
51
|
+
//# sourceMappingURL=watch-stream-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch-stream-parser.js","sourceRoot":"","sources":["../src/watch-stream-parser.ts"],"names":[],"mappings":";;;AAUA;;;GAGG;AACH,MAAa,iBAAiB;IACpB,MAAM,GAAG,EAAE,CAAC;IAEb,IAAI,CAAC,KAAa;QACvB,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC;QAErB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAEtC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;QAEhC,MAAM,MAAM,GAAkB,EAAE,CAAC;QAEjC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;YAEnC,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAEO,SAAS,CAAC,IAAY;QAC5B,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YACjB,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAyB,CAAC;YAExD,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;gBACnC,OAAO,SAAS,CAAC;YACnB,CAAC;YAED,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;QACtD,CAAC;QAAC,MAAM,CAAC;YACP,yFAAyF;YACzF,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;CACF;AAzCD,8CAyCC;AAED,MAAM,+BAA+B,GAAG,SAAS,CAAC;AAElD;;;GAGG;AACI,MAAM,6BAA6B,GAAG,CAAC,KAAkB,EAAW,EAAE;IAC3E,OAAO,CACL,KAAK,CAAC,IAAI,KAAK,OAAO;QACtB,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,+BAA+B,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,GAAG,CAAC,CACvF,CAAC;AACJ,CAAC,CAAC;AALW,QAAA,6BAA6B,iCAKxC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@digifi/k8s-grpc-js-resolver",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A grpc-js resolver that watches Kubernetes EndpointSlices for push-based service discovery.",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "DigiFi Team",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/**/*",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/digifi-io/k8s-grpc-js-resolver.git"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"registry": "https://registry.npmjs.org/"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
34
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
36
|
+
"test": "jest --runInBand",
|
|
37
|
+
"format": "prettier --write \"src/**/*.ts\" README.md package.json \"tsconfig*.json\" jest.config.cjs \".github/workflows/*.yml\"",
|
|
38
|
+
"format:check": "prettier --check \"src/**/*.ts\" README.md package.json \"tsconfig*.json\" jest.config.cjs \".github/workflows/*.yml\"",
|
|
39
|
+
"check": "npm run format:check && npm run typecheck && npm test && npm run build",
|
|
40
|
+
"prepack": "npm run check"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@grpc/grpc-js": "~1.14.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@grpc/grpc-js": "1.14.5",
|
|
47
|
+
"@swc/core": "1.16.2",
|
|
48
|
+
"@swc/jest": "0.2.39",
|
|
49
|
+
"@types/jest": "30.0.0",
|
|
50
|
+
"@types/node": "24.13.5",
|
|
51
|
+
"jest": "30.5.1",
|
|
52
|
+
"prettier": "3.9.8",
|
|
53
|
+
"typescript": "5.9.3"
|
|
54
|
+
}
|
|
55
|
+
}
|