fa-consul 1.0.1
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/README.md +112 -0
- package/dist/__tests__/lib/logger.d.ts +3 -0
- package/dist/__tests__/lib/logger.d.ts.map +1 -0
- package/dist/__tests__/lib/logger.js +25 -0
- package/dist/__tests__/lib/logger.js.map +1 -0
- package/dist/access-points/access-points-updater.d.ts +19 -0
- package/dist/access-points/access-points-updater.d.ts.map +1 -0
- package/dist/access-points/access-points-updater.js +141 -0
- package/dist/access-points/access-points-updater.js.map +1 -0
- package/dist/access-points/access-points-utils.d.ts +4 -0
- package/dist/access-points/access-points-utils.d.ts.map +1 -0
- package/dist/access-points/access-points-utils.js +27 -0
- package/dist/access-points/access-points-utils.js.map +1 -0
- package/dist/access-points/access-points.d.ts +20 -0
- package/dist/access-points/access-points.d.ts.map +1 -0
- package/dist/access-points/access-points.js +166 -0
- package/dist/access-points/access-points.js.map +1 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +7 -0
- package/dist/constants.js.map +1 -0
- package/dist/consul-client/endpoints/agent.d.ts +31 -0
- package/dist/consul-client/endpoints/agent.d.ts.map +1 -0
- package/dist/consul-client/endpoints/agent.js +87 -0
- package/dist/consul-client/endpoints/agent.js.map +1 -0
- package/dist/consul-client/endpoints/catalog.d.ts +11 -0
- package/dist/consul-client/endpoints/catalog.d.ts.map +1 -0
- package/dist/consul-client/endpoints/catalog.js +14 -0
- package/dist/consul-client/endpoints/catalog.js.map +1 -0
- package/dist/consul-client/endpoints/health.d.ts +18 -0
- package/dist/consul-client/endpoints/health.d.ts.map +1 -0
- package/dist/consul-client/endpoints/health.js +28 -0
- package/dist/consul-client/endpoints/health.js.map +1 -0
- package/dist/consul-client/http-client.d.ts +26 -0
- package/dist/consul-client/http-client.d.ts.map +1 -0
- package/dist/consul-client/http-client.js +139 -0
- package/dist/consul-client/http-client.js.map +1 -0
- package/dist/consul-client/index.d.ts +16 -0
- package/dist/consul-client/index.d.ts.map +1 -0
- package/dist/consul-client/index.js +25 -0
- package/dist/consul-client/index.js.map +1 -0
- package/dist/consul-client/types.d.ts +126 -0
- package/dist/consul-client/types.d.ts.map +1 -0
- package/dist/consul-client/types.js +2 -0
- package/dist/consul-client/types.js.map +1 -0
- package/dist/cyclic-register.d.ts +3 -0
- package/dist/cyclic-register.d.ts.map +1 -0
- package/dist/cyclic-register.js +75 -0
- package/dist/cyclic-register.js.map +1 -0
- package/dist/get-api.d.ts +5 -0
- package/dist/get-api.d.ts.map +1 -0
- package/dist/get-api.js +51 -0
- package/dist/get-api.js.map +1 -0
- package/dist/get-register-config.d.ts +4 -0
- package/dist/get-register-config.d.ts.map +1 -0
- package/dist/get-register-config.js +94 -0
- package/dist/get-register-config.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces.d.ts +251 -0
- package/dist/interfaces.d.ts.map +1 -0
- package/dist/interfaces.js +2 -0
- package/dist/interfaces.js.map +1 -0
- package/dist/lib/color.d.ts +40 -0
- package/dist/lib/color.d.ts.map +1 -0
- package/dist/lib/color.js +41 -0
- package/dist/lib/color.js.map +1 -0
- package/dist/lib/curl-text.d.ts +3 -0
- package/dist/lib/curl-text.d.ts.map +1 -0
- package/dist/lib/curl-text.js +73 -0
- package/dist/lib/curl-text.js.map +1 -0
- package/dist/lib/fqdn.d.ts +3 -0
- package/dist/lib/fqdn.d.ts.map +1 -0
- package/dist/lib/fqdn.js +40 -0
- package/dist/lib/fqdn.js.map +1 -0
- package/dist/lib/hash.d.ts +3 -0
- package/dist/lib/hash.d.ts.map +1 -0
- package/dist/lib/hash.js +64 -0
- package/dist/lib/hash.js.map +1 -0
- package/dist/lib/http-request-text.d.ts +4 -0
- package/dist/lib/http-request-text.d.ts.map +1 -0
- package/dist/lib/http-request-text.js +21 -0
- package/dist/lib/http-request-text.js.map +1 -0
- package/dist/lib/logger-stub.d.ts +9 -0
- package/dist/lib/logger-stub.d.ts.map +1 -0
- package/dist/lib/logger-stub.js +10 -0
- package/dist/lib/logger-stub.js.map +1 -0
- package/dist/lib/utils.d.ts +17 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +164 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/prepare-consul-api.d.ts +4 -0
- package/dist/prepare-consul-api.d.ts.map +1 -0
- package/dist/prepare-consul-api.js +380 -0
- package/dist/prepare-consul-api.js.map +1 -0
- package/dist/src/get-register-config.d.ts +4 -0
- package/dist/src/get-register-config.d.ts.map +1 -0
- package/dist/src/get-register-config.js +94 -0
- package/dist/src/get-register-config.js.map +1 -0
- package/package.json +65 -0
- package/src/access-points/access-points-updater.ts +154 -0
- package/src/access-points/access-points-utils.ts +31 -0
- package/src/access-points/access-points.ts +185 -0
- package/src/constants.ts +7 -0
- package/src/consul-client/endpoints/agent.ts +91 -0
- package/src/consul-client/endpoints/catalog.ts +13 -0
- package/src/consul-client/endpoints/health.ts +31 -0
- package/src/consul-client/http-client.ts +166 -0
- package/src/consul-client/index.ts +31 -0
- package/src/consul-client/types.ts +134 -0
- package/src/cyclic-register.ts +94 -0
- package/src/get-api.ts +62 -0
- package/src/get-register-config.ts +102 -0
- package/src/index.ts +58 -0
- package/src/interfaces.ts +276 -0
- package/src/lib/color.ts +43 -0
- package/src/lib/curl-text.ts +91 -0
- package/src/lib/fqdn.ts +45 -0
- package/src/lib/hash.ts +56 -0
- package/src/lib/http-request-text.ts +24 -0
- package/src/lib/logger-stub.ts +11 -0
- package/src/lib/utils.ts +174 -0
- package/src/prepare-consul-api.ts +426 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// noinspection JSUnusedGlobalSymbols
|
|
3
|
+
|
|
4
|
+
import { DEBUG, CONSUL_AP_UPDATE_TIMEOUT_MILLIS } from '../constants';
|
|
5
|
+
import { getConsulApiCached } from '../index';
|
|
6
|
+
import {
|
|
7
|
+
IAccessPoint,
|
|
8
|
+
IAccessPoints,
|
|
9
|
+
ICLOptions,
|
|
10
|
+
IConsulAPI,
|
|
11
|
+
IConsulHealthServiceInfo,
|
|
12
|
+
} from '../interfaces';
|
|
13
|
+
import { cyan, green, magenta, red, reset } from '../lib/color';
|
|
14
|
+
import loggerStub from '../lib/logger-stub';
|
|
15
|
+
import { sleep } from '../lib/utils';
|
|
16
|
+
|
|
17
|
+
const PREFIX = 'AP-UPDATER';
|
|
18
|
+
|
|
19
|
+
const dbg = { on: /\bAP-UPDATER\*?/i.test(DEBUG) || DEBUG === '*' };
|
|
20
|
+
const debug = (msg: string) => {
|
|
21
|
+
if (dbg.on) {
|
|
22
|
+
console.log(`${magenta}${PREFIX}${reset}: ${msg}`);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const UPDATE_INTERVAL_IF_CONSUL_REGISTER_SUCCESS_MILLIS = Number(process.env.UPDATE_INTERVAL_IF_CONSUL_REGISTER_SUCCESS_MILLIS) || (2 * 60_000);
|
|
27
|
+
|
|
28
|
+
// A stub in case such a function is not set for the access point in the configuration
|
|
29
|
+
function retrieveProps (accessPoint: IAccessPoint, host: string, meta?: any) {
|
|
30
|
+
const port = Number(meta?.port) || accessPoint.port;
|
|
31
|
+
return { host, port };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Служит для исключения повторного опроса consulID в пределах одного цикла updateAccessPoints
|
|
35
|
+
let oneUpdateCache: { [consulServiceName: string]: IConsulHealthServiceInfo[] } = {};
|
|
36
|
+
|
|
37
|
+
export async function updateAccessPoint (clOptions: ICLOptions, accessPoint: IAccessPoint): Promise<-2 | -1 | 0 | 1> {
|
|
38
|
+
if (!accessPoint.updateIntervalIfSuccessMillis) {
|
|
39
|
+
accessPoint.updateIntervalIfSuccessMillis = UPDATE_INTERVAL_IF_CONSUL_REGISTER_SUCCESS_MILLIS;
|
|
40
|
+
}
|
|
41
|
+
if (Date.now() - (accessPoint.lastSuccessUpdate || 0) < accessPoint.updateIntervalIfSuccessMillis) {
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
const { consulServiceName } = accessPoint;
|
|
45
|
+
const CONSUL_ID = `${cyan}${consulServiceName}${reset}`;
|
|
46
|
+
let result = oneUpdateCache[consulServiceName];
|
|
47
|
+
if (result) {
|
|
48
|
+
if (result.length) {
|
|
49
|
+
// Точка доступа уже опрошена в этом цикле и она была недоступна
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
// Точка доступа еще опрошена в этом цикле и есть сведения по ней. В этом просе будут взяты другие метаданные, нежели в предыдущем updateAccessPoint
|
|
53
|
+
} else {
|
|
54
|
+
// Точка доступа еще не опрошена в этом цикле
|
|
55
|
+
const consulApi: IConsulAPI = await getConsulApiCached(clOptions);
|
|
56
|
+
if (!consulApi) {
|
|
57
|
+
clOptions.logger?.warn(`${PREFIX}: Failed to get consul API`);
|
|
58
|
+
return -2;
|
|
59
|
+
}
|
|
60
|
+
debug(`${reset}Polling ${CONSUL_ID}`);
|
|
61
|
+
result = await consulApi.consulHealthService({ options: { service: consulServiceName, passing: true } });
|
|
62
|
+
oneUpdateCache[consulServiceName] = result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { Address: host, Meta: meta } = result?.[0]?.Service || {};
|
|
66
|
+
if (!host) {
|
|
67
|
+
clOptions.logger?.warn(`${red}There is no information for ${CONSUL_ID}`);
|
|
68
|
+
accessPoint.lastSuccessUpdate = 0;
|
|
69
|
+
const wasReachable = accessPoint.isReachable;
|
|
70
|
+
accessPoint.isReachable = false;
|
|
71
|
+
if (wasReachable) {
|
|
72
|
+
clOptions.em?.emit('access-point-updated', { accessPoint, changes: [['isReachable', true, false]] });
|
|
73
|
+
}
|
|
74
|
+
return -1;
|
|
75
|
+
}
|
|
76
|
+
accessPoint.isReachable = true;
|
|
77
|
+
accessPoint.lastSuccessUpdate = Date.now();
|
|
78
|
+
|
|
79
|
+
// If the retrieveProps function is not set for the access point in the configuration, use the stub
|
|
80
|
+
if (typeof accessPoint.retrieveProps !== 'function') {
|
|
81
|
+
accessPoint.retrieveProps = retrieveProps.bind(null, accessPoint);
|
|
82
|
+
}
|
|
83
|
+
const properties = accessPoint.retrieveProps!(host, meta);
|
|
84
|
+
const changes = accessPoint.setProps?.(properties)?.getChanges?.();
|
|
85
|
+
|
|
86
|
+
if (changes?.length) {
|
|
87
|
+
if (meta) {
|
|
88
|
+
accessPoint.meta = meta;
|
|
89
|
+
}
|
|
90
|
+
clOptions.em?.emit('access-point-updated', { accessPoint, changes });
|
|
91
|
+
} else {
|
|
92
|
+
debug(`${green}The data is up-to-date ${CONSUL_ID}`);
|
|
93
|
+
}
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function updateAccessPoints (clOptions: ICLOptions): Promise<boolean> {
|
|
98
|
+
const accessPoints = Object.values(<IAccessPoints>clOptions.config.accessPoints).filter((ap: any) => ap?.isAP && !ap.noConsul);
|
|
99
|
+
const result = [];
|
|
100
|
+
for (let i = 0; i < accessPoints.length; i++) {
|
|
101
|
+
const accessPoint = accessPoints[i];
|
|
102
|
+
if (!accessPoint) {continue;}
|
|
103
|
+
const res = await updateAccessPoint(clOptions, accessPoint);
|
|
104
|
+
result.push(res);
|
|
105
|
+
}
|
|
106
|
+
const updatedCount = result.filter((v) => v > 0);
|
|
107
|
+
if (updatedCount.length) {
|
|
108
|
+
clOptions.logger?.silly(`${PREFIX}: updated ${updatedCount.length} access point(s)`);
|
|
109
|
+
clOptions.em?.emit('access-points-updated');
|
|
110
|
+
}
|
|
111
|
+
return !!updatedCount;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const accessPointsUpdater = {
|
|
115
|
+
isStarted: false,
|
|
116
|
+
isAnyUpdated: false,
|
|
117
|
+
_timerId: setTimeout(() => null, 0),
|
|
118
|
+
_logger: loggerStub,
|
|
119
|
+
start (clOptions: ICLOptions, updateInterval: number = 10_000): number {
|
|
120
|
+
if (this.isStarted) {
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
this._logger = clOptions.logger || loggerStub;
|
|
124
|
+
const doLoop = async () => {
|
|
125
|
+
try {
|
|
126
|
+
oneUpdateCache = {};
|
|
127
|
+
const isAnyUpdated = await updateAccessPoints(clOptions);
|
|
128
|
+
if (isAnyUpdated) {
|
|
129
|
+
this.isAnyUpdated = true;
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
this._logger?.error(err);
|
|
133
|
+
}
|
|
134
|
+
clearTimeout(this._timerId);
|
|
135
|
+
this._timerId = setTimeout(doLoop, updateInterval);
|
|
136
|
+
};
|
|
137
|
+
doLoop().then((r) => r);
|
|
138
|
+
this.isStarted = true;
|
|
139
|
+
this._logger.info('Access point updater started');
|
|
140
|
+
return 1;
|
|
141
|
+
},
|
|
142
|
+
async waitForAnyUpdated (timeout: number = CONSUL_AP_UPDATE_TIMEOUT_MILLIS): Promise<boolean> {
|
|
143
|
+
const start = Date.now();
|
|
144
|
+
while (!this.isAnyUpdated && (Date.now() - start < timeout)) {
|
|
145
|
+
await sleep(100);
|
|
146
|
+
}
|
|
147
|
+
return this.isAnyUpdated;
|
|
148
|
+
},
|
|
149
|
+
stop () {
|
|
150
|
+
clearTimeout(this._timerId);
|
|
151
|
+
this.isStarted = false;
|
|
152
|
+
this._logger.info('Access point updater stopped');
|
|
153
|
+
},
|
|
154
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import * as https from 'https';
|
|
3
|
+
|
|
4
|
+
import { CONSUL_AP_UPDATE_TIMEOUT_MILLIS } from '../constants';
|
|
5
|
+
|
|
6
|
+
import { AccessPoints } from './access-points';
|
|
7
|
+
|
|
8
|
+
export const isHttpAvailable = (url: string) => new Promise((resolve) => {
|
|
9
|
+
const client = /^https:/i.test(url) ? https : http;
|
|
10
|
+
client.request(url, (r: any) => resolve(r.statusCode > 0)).on('error', () => resolve(false)).end();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const checkAccessPointAvailability = async (accessPoints: AccessPoints, accessPointId: string, onError: (errorMessage: string) => false) => {
|
|
14
|
+
const it = `Access point "${accessPointId}"`;
|
|
15
|
+
const ap = accessPoints.getAP(accessPointId);
|
|
16
|
+
if (!ap) {
|
|
17
|
+
return onError(`${it} is not found`);
|
|
18
|
+
}
|
|
19
|
+
if (!ap.waitForHostPortUpdated) {
|
|
20
|
+
return onError(`${it} has no method "waitForHostPortUpdated"`);
|
|
21
|
+
}
|
|
22
|
+
if (!(await ap.waitForHostPortUpdated(CONSUL_AP_UPDATE_TIMEOUT_MILLIS))) {
|
|
23
|
+
return onError(`${it} update timed out`);
|
|
24
|
+
}
|
|
25
|
+
const { host, port, protocol = 'http', path = '' } = ap;
|
|
26
|
+
const url = `${protocol}://${host}:${port}${path}`;
|
|
27
|
+
if (!(await isHttpAvailable(url))) {
|
|
28
|
+
return onError(`${it} is not available`);
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { CONSUL_AP_UPDATE_TIMEOUT_MILLIS } from '../constants';
|
|
2
|
+
import { IAccessPoint, IAccessPoints, ILogger } from '../interfaces';
|
|
3
|
+
import { blue, cyan, green, magenta, reset } from '../lib/color';
|
|
4
|
+
import loggerStub from '../lib/logger-stub';
|
|
5
|
+
import { isObject, sleep } from '../lib/utils';
|
|
6
|
+
|
|
7
|
+
const PREFIX = 'ACCESS-POINT';
|
|
8
|
+
|
|
9
|
+
const _logger_ = Symbol.for('_logger_');
|
|
10
|
+
|
|
11
|
+
const addAdditionalAPProps = (accessPoint: Record<string, any>) => {
|
|
12
|
+
if (accessPoint.noConsul) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(accessPoint, 'isAP', { value: true });
|
|
16
|
+
Object.defineProperty(accessPoint, 'lastSuccessUpdate', { value: 0, writable: true });
|
|
17
|
+
Object.defineProperty(accessPoint, 'idHostPortUpdated', { value: false, writable: true });
|
|
18
|
+
accessPoint.waitForHostPortUpdated = async (timeout: number = CONSUL_AP_UPDATE_TIMEOUT_MILLIS): Promise<boolean> => {
|
|
19
|
+
const start = Date.now();
|
|
20
|
+
while (!accessPoint.idHostPortUpdated && (Date.now() - start < timeout)) {
|
|
21
|
+
await sleep(100);
|
|
22
|
+
}
|
|
23
|
+
return !!accessPoint.idHostPortUpdated;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class AccessPoints {
|
|
28
|
+
private readonly [_logger_]: ILogger;
|
|
29
|
+
|
|
30
|
+
constructor (accessPoints: IAccessPoints, logger?: ILogger) {
|
|
31
|
+
this[_logger_] = logger || loggerStub;
|
|
32
|
+
if (!accessPoints) {
|
|
33
|
+
const msg = 'Empty argument "accessPoints" passed to constructor';
|
|
34
|
+
this[_logger_].error(msg);
|
|
35
|
+
throw new Error(msg);
|
|
36
|
+
}
|
|
37
|
+
Object.entries(accessPoints).forEach(([apKey, apData]) => {
|
|
38
|
+
this.addAP(apKey, apData);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static normalizePort (port: unknown) {
|
|
43
|
+
return Number(port) || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static normalizeProtocol (protocol: string | null) {
|
|
47
|
+
if (!protocol || !/^https?$/i.test(protocol)) {
|
|
48
|
+
protocol = 'http';
|
|
49
|
+
}
|
|
50
|
+
return protocol?.toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static normalizeValue (propName: string, propValue: any) {
|
|
54
|
+
switch (propName) {
|
|
55
|
+
case 'port':
|
|
56
|
+
return AccessPoints.normalizePort(propValue);
|
|
57
|
+
case 'protocol':
|
|
58
|
+
return AccessPoints.normalizeProtocol(propValue);
|
|
59
|
+
default:
|
|
60
|
+
return propValue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static getPureProps (accessPointSource: Record<string, any>): IAccessPoint {
|
|
65
|
+
const accessPoint = Object.create(null);
|
|
66
|
+
Object.entries(accessPointSource).forEach(([propName, propValue]) => {
|
|
67
|
+
if (propValue === undefined || typeof propValue === 'function') {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (typeof propValue === 'object' && propValue !== null) {
|
|
71
|
+
accessPoint[propName] = { ...propValue };
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
accessPoint[propName] = propValue;
|
|
75
|
+
});
|
|
76
|
+
return accessPoint;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
addAP (apKey: string, apData: any): IAccessPoint | undefined {
|
|
80
|
+
if (!apData || !isObject(apData)) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
if (apData.noConsul) {
|
|
84
|
+
// @ts-ignore
|
|
85
|
+
this[apKey] = apData;
|
|
86
|
+
return AccessPoints.getPureProps(apData);
|
|
87
|
+
}
|
|
88
|
+
if (!apData.consulServiceName) {
|
|
89
|
+
this[_logger_].error(`"${apKey}" access point not added because it lacks "consulServiceName" property`);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const accessPoint: Record<string, any> = {};
|
|
93
|
+
addAdditionalAPProps(accessPoint);
|
|
94
|
+
|
|
95
|
+
// @ts-ignore
|
|
96
|
+
this[apKey] = accessPoint;
|
|
97
|
+
Object.entries(apData).forEach(([propName, v]) => {
|
|
98
|
+
accessPoint[propName] = AccessPoints.normalizeValue(propName, v);
|
|
99
|
+
});
|
|
100
|
+
accessPoint.id = apKey;
|
|
101
|
+
accessPoint.title = accessPoint.title || apKey;
|
|
102
|
+
accessPoint.setProps = this.setAP.bind(this, apKey);
|
|
103
|
+
|
|
104
|
+
return AccessPoints.getPureProps(accessPoint);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setAP (apKey: string, apData: Record<string, any> | null): IAccessPoint | undefined {
|
|
108
|
+
if (!apData) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
// @ts-ignore
|
|
112
|
+
const accessPoint = this[apKey];
|
|
113
|
+
if (!accessPoint) {
|
|
114
|
+
return this.addAP(apKey, apData);
|
|
115
|
+
}
|
|
116
|
+
/* istanbul ignore if */
|
|
117
|
+
if (!accessPoint.isAP) {
|
|
118
|
+
addAdditionalAPProps(accessPoint);
|
|
119
|
+
}
|
|
120
|
+
const was: string[] = [];
|
|
121
|
+
const became: string[] = [];
|
|
122
|
+
const changes: any[] = [];
|
|
123
|
+
|
|
124
|
+
const msgVal = (propName: string, propValue: any, valueColor: string) => {
|
|
125
|
+
const ret = (v: any, color = blue) => `${cyan}${propName}${reset}: ${color}${v}${reset}`;
|
|
126
|
+
return (propValue == null || propValue === '') ? ret(`[${String(propValue)}]`) : ret(propValue, valueColor);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
Object.entries(apData).forEach(([propName, newV]) => {
|
|
130
|
+
if (newV === undefined) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const oldV = accessPoint[propName];
|
|
134
|
+
newV = AccessPoints.normalizeValue(propName, newV);
|
|
135
|
+
if (oldV !== newV) {
|
|
136
|
+
was.push(msgVal(propName, oldV, magenta));
|
|
137
|
+
became.push(msgVal(propName, newV, green));
|
|
138
|
+
changes.push([propName, oldV, newV]);
|
|
139
|
+
}
|
|
140
|
+
accessPoint[propName] = newV;
|
|
141
|
+
});
|
|
142
|
+
if (was.length) {
|
|
143
|
+
this[_logger_].info(`${PREFIX}: Change AP ${green}${accessPoint.id}${reset}/${cyan}${accessPoint.consulServiceName}${reset} from ${was.join('; ')} to ${became.join('; ')}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
accessPoint.idHostPortUpdated = accessPoint.idHostPortUpdated
|
|
147
|
+
|| !!(accessPoint.host && accessPoint.port && (apData.host || apData.port));
|
|
148
|
+
|
|
149
|
+
const result = AccessPoints.getPureProps(accessPoint);
|
|
150
|
+
result.getChanges = () => (changes.length ? changes : undefined);
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getAP (accessPointKey: string, andNotIsAP?: boolean): IAccessPoint | undefined {
|
|
155
|
+
if (accessPointKey) {
|
|
156
|
+
// @ts-ignore
|
|
157
|
+
const accessPoint = this[accessPointKey];
|
|
158
|
+
if (!andNotIsAP && !accessPoint?.isAP) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
return accessPoint;
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Если передан accessPointKey, то возвращается этот AP, если есть.
|
|
168
|
+
* Если accessPointKey НЕ передан, то возвращаются ВСЕ AP
|
|
169
|
+
*/
|
|
170
|
+
get (accessPointKey?: string, andNotIsAP?: boolean): IAccessPoints | IAccessPoint | undefined {
|
|
171
|
+
if (accessPointKey) {
|
|
172
|
+
// @ts-ignore
|
|
173
|
+
const accessPoint = this[accessPointKey];
|
|
174
|
+
if (!accessPoint || (!andNotIsAP && !accessPoint?.isAP)) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
return AccessPoints.getPureProps(accessPoint);
|
|
178
|
+
}
|
|
179
|
+
const accessPoints = Object.create(null) as IAccessPoints;
|
|
180
|
+
Object.values(this).filter((ap) => ap?.isAP).forEach((accessPoint) => {
|
|
181
|
+
accessPoints[accessPoint.id] = AccessPoints.getPureProps(accessPoint);
|
|
182
|
+
});
|
|
183
|
+
return accessPoints;
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const PREFIX = 'AF-CONSUL';
|
|
2
|
+
export const MAX_API_CACHED = 3;
|
|
3
|
+
|
|
4
|
+
export const CONSUL_AP_UPDATE_TIMEOUT_MILLIS = Number(process.env.CONSUL_AP_UPDATE_TIMEOUT_MILLIS) || 10_000;
|
|
5
|
+
export const DEBUG = (String(process.env.DEBUG || '')).trim();
|
|
6
|
+
export const CONSUL_DEBUG_ON = DEBUG.split(/[ ,]/).some((v) => /af-consul/i.test(v) || v === '*');
|
|
7
|
+
export const FORCE_EVERY_REGISTER_ATTEMPT = !!process.env.FORCE_EVERY_REGISTER_ATTEMPT;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ConsulHttpClient } from '../http-client';
|
|
2
|
+
import { ServiceInfo, RegisterServiceOptions, AgentMember, RegisterCheck } from '../types';
|
|
3
|
+
|
|
4
|
+
export class AgentAPI {
|
|
5
|
+
constructor (private client: ConsulHttpClient) {}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /v1/agent/services
|
|
9
|
+
* Returns all services registered with the local agent
|
|
10
|
+
*/
|
|
11
|
+
async serviceList (): Promise<Record<string, ServiceInfo>> {
|
|
12
|
+
return this.client.get<Record<string, ServiceInfo>>('/agent/services');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* PUT /v1/agent/service/register
|
|
17
|
+
* Registers a new service with the local agent
|
|
18
|
+
*/
|
|
19
|
+
async serviceRegister (options: RegisterServiceOptions): Promise<void> {
|
|
20
|
+
// Convert to Consul API format (PascalCase)
|
|
21
|
+
const body: Record<string, unknown> = {
|
|
22
|
+
ID: options.id,
|
|
23
|
+
Name: options.name,
|
|
24
|
+
Tags: options.tags,
|
|
25
|
+
Address: options.address,
|
|
26
|
+
Port: options.port,
|
|
27
|
+
Meta: options.meta,
|
|
28
|
+
Check: options.check ? this.formatCheck(options.check) : undefined,
|
|
29
|
+
Checks: options.checks?.map((c) => this.formatCheck(c)),
|
|
30
|
+
Connect: options.connect,
|
|
31
|
+
Proxy: options.proxy,
|
|
32
|
+
TaggedAddresses: options.taggedAddresses,
|
|
33
|
+
Weights: options.weights,
|
|
34
|
+
EnableTagOverride: options.enableTagOverride,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Remove undefined values
|
|
38
|
+
Object.keys(body).forEach((key) => {
|
|
39
|
+
if (body[key] === undefined) {
|
|
40
|
+
delete body[key];
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await this.client.put<void>('/agent/service/register', body);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* PUT /v1/agent/service/deregister/:service_id
|
|
49
|
+
* Deregisters a service from the local agent
|
|
50
|
+
*/
|
|
51
|
+
async serviceDeregister (serviceId: string): Promise<void> {
|
|
52
|
+
await this.client.put<void>(`/agent/service/deregister/${encodeURIComponent(serviceId)}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* GET /v1/agent/members
|
|
57
|
+
* Returns the members the agent sees in the cluster
|
|
58
|
+
*/
|
|
59
|
+
async members (options?: { wan?: boolean; segment?: string }): Promise<AgentMember[]> {
|
|
60
|
+
const query: Record<string, string | boolean | undefined> = {};
|
|
61
|
+
if (options?.wan) {query.wan = options.wan;}
|
|
62
|
+
if (options?.segment) {query.segment = options.segment;}
|
|
63
|
+
return this.client.get<AgentMember[]>('/agent/members', Object.keys(query).length ? query : undefined);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private formatCheck (check: RegisterCheck): Record<string, unknown> {
|
|
67
|
+
const formatted: Record<string, unknown> = {
|
|
68
|
+
Name: check.name,
|
|
69
|
+
HTTP: check.http,
|
|
70
|
+
TCP: check.tcp,
|
|
71
|
+
Script: check.script,
|
|
72
|
+
Shell: check.shell,
|
|
73
|
+
DockerContainerID: check.dockercontainerid,
|
|
74
|
+
Interval: check.interval,
|
|
75
|
+
Timeout: check.timeout,
|
|
76
|
+
TTL: check.ttl,
|
|
77
|
+
Notes: check.notes,
|
|
78
|
+
Status: check.status,
|
|
79
|
+
DeregisterCriticalServiceAfter: check.deregistercriticalserviceafter,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Remove undefined values
|
|
83
|
+
Object.keys(formatted).forEach((key) => {
|
|
84
|
+
if (formatted[key] === undefined) {
|
|
85
|
+
delete formatted[key];
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return formatted;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ConsulHttpClient } from '../http-client';
|
|
2
|
+
|
|
3
|
+
export class CatalogAPI {
|
|
4
|
+
constructor (private client: ConsulHttpClient) {}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GET /v1/catalog/services
|
|
8
|
+
* Returns all services in a datacenter
|
|
9
|
+
*/
|
|
10
|
+
async serviceList (dc?: string): Promise<Record<string, string[]>> {
|
|
11
|
+
return this.client.get<Record<string, string[]>>('/catalog/services', dc ? { dc } : undefined);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ConsulHttpClient } from '../http-client';
|
|
2
|
+
import { HealthServiceInfo } from '../types';
|
|
3
|
+
|
|
4
|
+
export class HealthAPI {
|
|
5
|
+
constructor (private client: ConsulHttpClient) {}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /v1/health/service/:service
|
|
9
|
+
* Returns the nodes and health info of a service
|
|
10
|
+
*/
|
|
11
|
+
async service (options: {
|
|
12
|
+
service: string;
|
|
13
|
+
dc?: string;
|
|
14
|
+
passing?: boolean;
|
|
15
|
+
tag?: string;
|
|
16
|
+
near?: string;
|
|
17
|
+
}): Promise<HealthServiceInfo[]> {
|
|
18
|
+
const { service, ...queryOptions } = options;
|
|
19
|
+
const query: Record<string, string | boolean | undefined> = {};
|
|
20
|
+
|
|
21
|
+
if (queryOptions.dc) {query.dc = queryOptions.dc;}
|
|
22
|
+
if (queryOptions.passing !== undefined) {query.passing = queryOptions.passing;}
|
|
23
|
+
if (queryOptions.tag) {query.tag = queryOptions.tag;}
|
|
24
|
+
if (queryOptions.near) {query.near = queryOptions.near;}
|
|
25
|
+
|
|
26
|
+
return this.client.get<HealthServiceInfo[]>(
|
|
27
|
+
`/health/service/${encodeURIComponent(service)}`,
|
|
28
|
+
Object.keys(query).length ? query : undefined,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import * as https from 'https';
|
|
3
|
+
|
|
4
|
+
import { ConsulClientOptions, RequestInfo, ResponseInfo, OnRequestHook, OnResponseHook } from './types';
|
|
5
|
+
|
|
6
|
+
export class ConsulHttpClient {
|
|
7
|
+
private readonly baseUrl: string;
|
|
8
|
+
private readonly headers: Record<string, string>;
|
|
9
|
+
private readonly timeout: number;
|
|
10
|
+
private readonly httpModule: typeof http | typeof https;
|
|
11
|
+
private readonly host: string;
|
|
12
|
+
private readonly port: string | number;
|
|
13
|
+
private readonly protocol: string;
|
|
14
|
+
|
|
15
|
+
private requestCounter = 0;
|
|
16
|
+
private onRequestHooks: OnRequestHook[] = [];
|
|
17
|
+
private onResponseHooks: OnResponseHook[] = [];
|
|
18
|
+
|
|
19
|
+
constructor (options: ConsulClientOptions) {
|
|
20
|
+
const protocol = options.secure ? 'https' : 'http';
|
|
21
|
+
const port = options.port || (options.secure ? 443 : 8500);
|
|
22
|
+
this.host = options.host;
|
|
23
|
+
this.port = port;
|
|
24
|
+
this.protocol = `${protocol}:`;
|
|
25
|
+
this.baseUrl = `${protocol}://${options.host}:${port}/v1`;
|
|
26
|
+
this.httpModule = options.secure ? https : http;
|
|
27
|
+
this.timeout = options.timeout || 30000;
|
|
28
|
+
|
|
29
|
+
this.headers = {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
Accept: 'application/json',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Support both direct token and defaults.token (for compatibility)
|
|
35
|
+
const token = options.token || options.defaults?.token;
|
|
36
|
+
if (token) {
|
|
37
|
+
this.headers['X-Consul-Token'] = token;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onRequest (hook: OnRequestHook): void {
|
|
42
|
+
this.onRequestHooks.push(hook);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onResponse (hook: OnResponseHook): void {
|
|
46
|
+
this.onResponseHooks.push(hook);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private buildQueryString (params?: Record<string, string | boolean | undefined>): string {
|
|
50
|
+
if (!params) {return '';}
|
|
51
|
+
const entries = Object.entries(params)
|
|
52
|
+
.filter(([, v]) => v !== undefined && v !== '')
|
|
53
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
54
|
+
return entries.length ? `?${entries.join('&')}` : '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async request<T> (
|
|
58
|
+
method: 'GET' | 'PUT' | 'POST' | 'DELETE',
|
|
59
|
+
path: string,
|
|
60
|
+
options?: {
|
|
61
|
+
query?: Record<string, string | boolean | undefined>;
|
|
62
|
+
body?: unknown;
|
|
63
|
+
skipCodes?: number[];
|
|
64
|
+
},
|
|
65
|
+
): Promise<T> {
|
|
66
|
+
const requestId = ++this.requestCounter;
|
|
67
|
+
const queryString = this.buildQueryString(options?.query);
|
|
68
|
+
const url = `${this.baseUrl}${path}${queryString}`;
|
|
69
|
+
const body = options?.body ? JSON.stringify(options.body) : undefined;
|
|
70
|
+
|
|
71
|
+
const requestInfo: RequestInfo = {
|
|
72
|
+
id: requestId,
|
|
73
|
+
method,
|
|
74
|
+
url,
|
|
75
|
+
headers: { ...this.headers },
|
|
76
|
+
body,
|
|
77
|
+
timestamp: Date.now(),
|
|
78
|
+
// Properties for compatibility with debug utilities
|
|
79
|
+
path: `/v1${path}${queryString}`,
|
|
80
|
+
hostname: this.host,
|
|
81
|
+
port: this.port,
|
|
82
|
+
protocol: this.protocol,
|
|
83
|
+
query: options?.query,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Emit onRequest hooks
|
|
87
|
+
this.onRequestHooks.forEach((hook) => hook(requestInfo));
|
|
88
|
+
|
|
89
|
+
return new Promise<T>((resolve, reject) => {
|
|
90
|
+
const urlObj = new URL(url);
|
|
91
|
+
|
|
92
|
+
const reqOptions: http.RequestOptions = {
|
|
93
|
+
hostname: urlObj.hostname,
|
|
94
|
+
port: urlObj.port,
|
|
95
|
+
path: urlObj.pathname + urlObj.search,
|
|
96
|
+
method,
|
|
97
|
+
headers: {
|
|
98
|
+
...this.headers,
|
|
99
|
+
...(body ? { 'Content-Length': Buffer.byteLength(body) } : {}),
|
|
100
|
+
},
|
|
101
|
+
timeout: this.timeout,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const req = this.httpModule.request(reqOptions, (res) => {
|
|
105
|
+
let data = '';
|
|
106
|
+
res.on('data', (chunk) => {
|
|
107
|
+
data += chunk;
|
|
108
|
+
});
|
|
109
|
+
res.on('end', () => {
|
|
110
|
+
const statusCode = res.statusCode || 0;
|
|
111
|
+
let parsedBody: unknown;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
parsedBody = data ? JSON.parse(data) : null;
|
|
115
|
+
} catch {
|
|
116
|
+
parsedBody = data;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const responseInfo: ResponseInfo = {
|
|
120
|
+
requestId,
|
|
121
|
+
statusCode,
|
|
122
|
+
body: parsedBody,
|
|
123
|
+
timestamp: Date.now(),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Emit onResponse hooks
|
|
127
|
+
this.onResponseHooks.forEach((hook) => hook(requestInfo, responseInfo));
|
|
128
|
+
|
|
129
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
130
|
+
resolve(parsedBody as T);
|
|
131
|
+
} else if (options?.skipCodes?.includes(statusCode)) {
|
|
132
|
+
resolve(parsedBody as T);
|
|
133
|
+
} else {
|
|
134
|
+
reject(new Error(`Consul API error: ${statusCode} - ${JSON.stringify(parsedBody)}`));
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
req.on('error', reject);
|
|
140
|
+
req.on('timeout', () => {
|
|
141
|
+
req.destroy();
|
|
142
|
+
reject(new Error(`Request timeout: ${url}`));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (body) {
|
|
146
|
+
req.write(body);
|
|
147
|
+
}
|
|
148
|
+
req.end();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async get<T> (path: string, query?: Record<string, string | boolean | undefined>): Promise<T> {
|
|
153
|
+
return this.request<T>('GET', path, query ? { query } : undefined);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async put<T> (path: string, body?: unknown, query?: Record<string, string | boolean | undefined>): Promise<T> {
|
|
157
|
+
return this.request<T>('PUT', path, {
|
|
158
|
+
...(query ? { query } : {}),
|
|
159
|
+
...(body !== undefined ? { body } : {}),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getRequestCounter (): number {
|
|
164
|
+
return this.requestCounter;
|
|
165
|
+
}
|
|
166
|
+
}
|