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.
Files changed (125) hide show
  1. package/README.md +112 -0
  2. package/dist/__tests__/lib/logger.d.ts +3 -0
  3. package/dist/__tests__/lib/logger.d.ts.map +1 -0
  4. package/dist/__tests__/lib/logger.js +25 -0
  5. package/dist/__tests__/lib/logger.js.map +1 -0
  6. package/dist/access-points/access-points-updater.d.ts +19 -0
  7. package/dist/access-points/access-points-updater.d.ts.map +1 -0
  8. package/dist/access-points/access-points-updater.js +141 -0
  9. package/dist/access-points/access-points-updater.js.map +1 -0
  10. package/dist/access-points/access-points-utils.d.ts +4 -0
  11. package/dist/access-points/access-points-utils.d.ts.map +1 -0
  12. package/dist/access-points/access-points-utils.js +27 -0
  13. package/dist/access-points/access-points-utils.js.map +1 -0
  14. package/dist/access-points/access-points.d.ts +20 -0
  15. package/dist/access-points/access-points.d.ts.map +1 -0
  16. package/dist/access-points/access-points.js +166 -0
  17. package/dist/access-points/access-points.js.map +1 -0
  18. package/dist/constants.d.ts +7 -0
  19. package/dist/constants.d.ts.map +1 -0
  20. package/dist/constants.js +7 -0
  21. package/dist/constants.js.map +1 -0
  22. package/dist/consul-client/endpoints/agent.d.ts +31 -0
  23. package/dist/consul-client/endpoints/agent.d.ts.map +1 -0
  24. package/dist/consul-client/endpoints/agent.js +87 -0
  25. package/dist/consul-client/endpoints/agent.js.map +1 -0
  26. package/dist/consul-client/endpoints/catalog.d.ts +11 -0
  27. package/dist/consul-client/endpoints/catalog.d.ts.map +1 -0
  28. package/dist/consul-client/endpoints/catalog.js +14 -0
  29. package/dist/consul-client/endpoints/catalog.js.map +1 -0
  30. package/dist/consul-client/endpoints/health.d.ts +18 -0
  31. package/dist/consul-client/endpoints/health.d.ts.map +1 -0
  32. package/dist/consul-client/endpoints/health.js +28 -0
  33. package/dist/consul-client/endpoints/health.js.map +1 -0
  34. package/dist/consul-client/http-client.d.ts +26 -0
  35. package/dist/consul-client/http-client.d.ts.map +1 -0
  36. package/dist/consul-client/http-client.js +139 -0
  37. package/dist/consul-client/http-client.js.map +1 -0
  38. package/dist/consul-client/index.d.ts +16 -0
  39. package/dist/consul-client/index.d.ts.map +1 -0
  40. package/dist/consul-client/index.js +25 -0
  41. package/dist/consul-client/index.js.map +1 -0
  42. package/dist/consul-client/types.d.ts +126 -0
  43. package/dist/consul-client/types.d.ts.map +1 -0
  44. package/dist/consul-client/types.js +2 -0
  45. package/dist/consul-client/types.js.map +1 -0
  46. package/dist/cyclic-register.d.ts +3 -0
  47. package/dist/cyclic-register.d.ts.map +1 -0
  48. package/dist/cyclic-register.js +75 -0
  49. package/dist/cyclic-register.js.map +1 -0
  50. package/dist/get-api.d.ts +5 -0
  51. package/dist/get-api.d.ts.map +1 -0
  52. package/dist/get-api.js +51 -0
  53. package/dist/get-api.js.map +1 -0
  54. package/dist/get-register-config.d.ts +4 -0
  55. package/dist/get-register-config.d.ts.map +1 -0
  56. package/dist/get-register-config.js +94 -0
  57. package/dist/get-register-config.js.map +1 -0
  58. package/dist/index.d.ts +12 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +11 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/interfaces.d.ts +251 -0
  63. package/dist/interfaces.d.ts.map +1 -0
  64. package/dist/interfaces.js +2 -0
  65. package/dist/interfaces.js.map +1 -0
  66. package/dist/lib/color.d.ts +40 -0
  67. package/dist/lib/color.d.ts.map +1 -0
  68. package/dist/lib/color.js +41 -0
  69. package/dist/lib/color.js.map +1 -0
  70. package/dist/lib/curl-text.d.ts +3 -0
  71. package/dist/lib/curl-text.d.ts.map +1 -0
  72. package/dist/lib/curl-text.js +73 -0
  73. package/dist/lib/curl-text.js.map +1 -0
  74. package/dist/lib/fqdn.d.ts +3 -0
  75. package/dist/lib/fqdn.d.ts.map +1 -0
  76. package/dist/lib/fqdn.js +40 -0
  77. package/dist/lib/fqdn.js.map +1 -0
  78. package/dist/lib/hash.d.ts +3 -0
  79. package/dist/lib/hash.d.ts.map +1 -0
  80. package/dist/lib/hash.js +64 -0
  81. package/dist/lib/hash.js.map +1 -0
  82. package/dist/lib/http-request-text.d.ts +4 -0
  83. package/dist/lib/http-request-text.d.ts.map +1 -0
  84. package/dist/lib/http-request-text.js +21 -0
  85. package/dist/lib/http-request-text.js.map +1 -0
  86. package/dist/lib/logger-stub.d.ts +9 -0
  87. package/dist/lib/logger-stub.d.ts.map +1 -0
  88. package/dist/lib/logger-stub.js +10 -0
  89. package/dist/lib/logger-stub.js.map +1 -0
  90. package/dist/lib/utils.d.ts +17 -0
  91. package/dist/lib/utils.d.ts.map +1 -0
  92. package/dist/lib/utils.js +164 -0
  93. package/dist/lib/utils.js.map +1 -0
  94. package/dist/prepare-consul-api.d.ts +4 -0
  95. package/dist/prepare-consul-api.d.ts.map +1 -0
  96. package/dist/prepare-consul-api.js +380 -0
  97. package/dist/prepare-consul-api.js.map +1 -0
  98. package/dist/src/get-register-config.d.ts +4 -0
  99. package/dist/src/get-register-config.d.ts.map +1 -0
  100. package/dist/src/get-register-config.js +94 -0
  101. package/dist/src/get-register-config.js.map +1 -0
  102. package/package.json +65 -0
  103. package/src/access-points/access-points-updater.ts +154 -0
  104. package/src/access-points/access-points-utils.ts +31 -0
  105. package/src/access-points/access-points.ts +185 -0
  106. package/src/constants.ts +7 -0
  107. package/src/consul-client/endpoints/agent.ts +91 -0
  108. package/src/consul-client/endpoints/catalog.ts +13 -0
  109. package/src/consul-client/endpoints/health.ts +31 -0
  110. package/src/consul-client/http-client.ts +166 -0
  111. package/src/consul-client/index.ts +31 -0
  112. package/src/consul-client/types.ts +134 -0
  113. package/src/cyclic-register.ts +94 -0
  114. package/src/get-api.ts +62 -0
  115. package/src/get-register-config.ts +102 -0
  116. package/src/index.ts +58 -0
  117. package/src/interfaces.ts +276 -0
  118. package/src/lib/color.ts +43 -0
  119. package/src/lib/curl-text.ts +91 -0
  120. package/src/lib/fqdn.ts +45 -0
  121. package/src/lib/hash.ts +56 -0
  122. package/src/lib/http-request-text.ts +24 -0
  123. package/src/lib/logger-stub.ts +11 -0
  124. package/src/lib/utils.ts +174 -0
  125. package/src/prepare-consul-api.ts +426 -0
@@ -0,0 +1,174 @@
1
+ import * as fs from 'fs';
2
+
3
+ import { ICache, IConsulServiceInfo, IRegisterConfig } from '../interfaces';
4
+
5
+ export const removeAroundQuotas = (str: string): string => {
6
+ if (!str) {
7
+ return str;
8
+ }
9
+ const re = /^(["'])([^\r\n]+)(\1)$/;
10
+ while (re.test(str)) {
11
+ str = str.replace(re, '$2');
12
+ }
13
+ return str;
14
+ };
15
+
16
+ export const parseBoolean = (bv: any): boolean => {
17
+ if (typeof bv === 'boolean' || typeof bv === 'number') {
18
+ return !!bv;
19
+ }
20
+ if (typeof bv !== 'string') {
21
+ bv = String(bv);
22
+ }
23
+ return !/^(false|no|0)$/i.test(bv.trim().toLowerCase());
24
+ };
25
+
26
+ export const substitutePercentBracket = (v: string, data: any): string => {
27
+ const re = /%?{([^}]+)}/g;
28
+ let result: string = v;
29
+ const matches = [...v.matchAll(re)];
30
+ matches.forEach(([found, propName]) => {
31
+ const substitution = String(data[propName!] || '');
32
+ result = result.replace(found, substitution);
33
+ });
34
+ return result;
35
+ };
36
+
37
+ export const substitute = (meta: any, data: any): void => {
38
+ Object.entries(meta).forEach(([k, v]) => {
39
+ if (typeof v === 'string') {
40
+ meta[k] = substitutePercentBracket(v, data);
41
+ }
42
+ });
43
+ };
44
+
45
+ export const parseMeta = (m: string | object | undefined, data: object) => {
46
+ const metaData = {} as any;
47
+ if (!m) {
48
+ return metaData;
49
+ }
50
+ const fillMetaData = (o: object) => {
51
+ Object.entries(o).forEach(([k, v]) => {
52
+ if (!['string', 'number'].includes(typeof v)) {
53
+ v = String(v);
54
+ }
55
+ if (/^[A-Z][A-Z_\d]+$/i.test(k)) {
56
+ metaData[k] = v;
57
+ }
58
+ });
59
+ };
60
+
61
+ if (typeof m === 'string') {
62
+ m = removeAroundQuotas(m);
63
+ if (m.startsWith('{')) {
64
+ try {
65
+ fillMetaData(JSON.parse(m));
66
+ } catch { //
67
+ }
68
+ } else if (m.includes('=')) {
69
+ m.split(/;/g).forEach((pair) => {
70
+ const i = pair.indexOf('=');
71
+ if (i < 0) {
72
+ return;
73
+ }
74
+ const k = pair.substring(0, i).trim();
75
+ const v = pair.substring(i + 1).trim();
76
+ if (k) {
77
+ metaData[k] = v;
78
+ }
79
+ });
80
+ }
81
+ } else if (typeof m === 'object' && !Array.isArray(m)) {
82
+ fillMetaData(m);
83
+ }
84
+ substitute(metaData, data);
85
+ return metaData;
86
+ };
87
+
88
+ export const parseTags = (t: any): string[] => {
89
+ if (typeof t === 'string') {
90
+ t = removeAroundQuotas(t);
91
+ return t.split(/;/g).map((v: string) => v.trim()).filter((v: string) => v);
92
+ }
93
+ if (typeof t === 'number') {
94
+ return [String(t)];
95
+ }
96
+ if (Array.isArray(t)) {
97
+ return t.map((v) => String(v).trim()).filter((v) => v);
98
+ }
99
+ return [];
100
+ };
101
+
102
+ export const serviceConfigDiff = (registerConfig: IRegisterConfig, serviceInfo: IConsulServiceInfo | undefined): any[] => {
103
+ if (!serviceInfo) {
104
+ return ['id', registerConfig.id, 'ID', undefined];
105
+ }
106
+ const mastBeEquals = [['id', 'ID'], ['name', 'Service'], ['port', 'Port'], ['address', 'Address']];
107
+ let diff: any[] = [];
108
+ mastBeEquals.some(([p1, p2]) => {
109
+ if (registerConfig[p1 as keyof IRegisterConfig] !== serviceInfo[p2!]) {
110
+ diff = [p1, registerConfig[p1 as keyof IRegisterConfig], p2, serviceInfo[p2!]];
111
+ return true;
112
+ }
113
+ return false;
114
+ });
115
+ if (!diff.length) {
116
+ const { meta } = registerConfig;
117
+ const { Meta = {} } = serviceInfo;
118
+ Object.entries(meta as { [s: string]: string }).some(([p, v]) => {
119
+ if (v !== Meta[p]) {
120
+ diff = [`meta.${p}`, v, `Meta.${p}`, Meta[p]];
121
+ return true;
122
+ }
123
+ return false;
124
+ });
125
+ }
126
+ return diff;
127
+ };
128
+
129
+ export const minimizeCache = <T> (cache: ICache<T>, maxItems: number) => {
130
+ const len = Object.keys(cache).length;
131
+ if (len >= maxItems) {
132
+ const sortedDesc = Object.entries(cache)
133
+ .sort((a, b) => b[1].created - a[1].created);
134
+ sortedDesc.splice(0, maxItems - 1);
135
+ sortedDesc.map(([h]) => h).forEach((h) => {
136
+ delete cache[h];
137
+ });
138
+ }
139
+ };
140
+
141
+ /**
142
+ * String in format \d+(s|m) in milliseconds
143
+ */
144
+ export const toMills = (timeStr: string = ''): number => {
145
+ const re = /^(\d+)([sm])$/;
146
+ const matches = re.exec(timeStr);
147
+ if (!matches) {
148
+ return 0;
149
+ }
150
+ return Number(matches[1]) * 1000 * (matches[2] === 's' ? 1 : 60);
151
+ };
152
+
153
+ export const getPackageJson = (relPathToProjRoot: string = '') => {
154
+ try {
155
+ const rootDir = process.cwd();
156
+ const packageJson = `${rootDir}${relPathToProjRoot}/package.json`;
157
+ if (fs.existsSync(packageJson)) {
158
+ return JSON.parse(fs.readFileSync(packageJson, { encoding: 'utf8' }));
159
+ }
160
+ } catch {
161
+ //
162
+ }
163
+ };
164
+
165
+ export const sleep = async (timeOut: number) => new Promise((resolve) => {
166
+ setTimeout(resolve, timeOut);
167
+ });
168
+
169
+ export const isObject = (v: any): boolean => v != null
170
+ && typeof v === 'object'
171
+ && !Array.isArray(v)
172
+ && !(v instanceof Date)
173
+ && !(v instanceof Set)
174
+ && !(v instanceof Map);
@@ -0,0 +1,426 @@
1
+ /* eslint-disable no-console */
2
+ // noinspection UnnecessaryLocalVariableJS,JSUnusedGlobalSymbols
3
+
4
+ import { Mutex } from 'async-mutex';
5
+
6
+ import { CONSUL_DEBUG_ON, DEBUG, MAX_API_CACHED, PREFIX } from './constants';
7
+ import { ConsulClient, RequestInfo, ResponseInfo } from './consul-client';
8
+ import {
9
+ IAPIArgs,
10
+ ICache,
11
+ ICLOptions,
12
+ IConsulAgentConfig,
13
+ IConsulAgentOptions,
14
+ IConsulAPI,
15
+ IConsulHealthServiceInfo,
16
+ IConsulServiceInfo,
17
+ IFullConsulAgentConfig,
18
+ IFullConsulAgentOptions,
19
+ ILogger,
20
+ IRegisterConfig,
21
+ IRegisterOptions,
22
+ ISocketInfo,
23
+ TRegisterResult,
24
+ } from './interfaces';
25
+ import { blue, cyan, magenta, reset, yellow } from './lib/color';
26
+ import { getCurlText } from './lib/curl-text';
27
+ import { getFQDNCached } from './lib/fqdn';
28
+ import { getConfigHash } from './lib/hash';
29
+ import getHttpRequestText from './lib/http-request-text';
30
+ import loggerStub from './lib/logger-stub';
31
+ import { minimizeCache, parseBoolean, serviceConfigDiff } from './lib/utils';
32
+
33
+ const mutex = new Mutex();
34
+
35
+ const dbg = {
36
+ on: CONSUL_DEBUG_ON,
37
+ curl: /af-consul:curl/i.test(DEBUG),
38
+ };
39
+ const debug = (msg: string) => {
40
+ if (dbg.on) {
41
+ console.log(`${magenta}${PREFIX}${reset}: ${msg}`);
42
+ }
43
+ };
44
+
45
+ const agentTypeS = Symbol.for('agentType');
46
+
47
+ const consulConfigTypes = ['reg', 'dev', 'prd'] as (keyof IFullConsulAgentConfig)[];
48
+
49
+ const getConsulAgentOptions = async (clOptions: ICLOptions): Promise<IFullConsulAgentOptions> => {
50
+ const {
51
+ agent,
52
+ service,
53
+ } = clOptions.config.consul;
54
+
55
+ const secure_ = parseBoolean(agent.reg.secure);
56
+ const result: IFullConsulAgentOptions = {} as IFullConsulAgentOptions;
57
+ const reg: IConsulAgentOptions = {
58
+ host: agent.reg.host || (await getFQDNCached()) || process.env.HOST_HOSTNAME || service?.host || '127.0.0.1',
59
+ port: String(agent.reg.port || (secure_ ? 433 : 8500)),
60
+ secure: secure_,
61
+ ...(agent.reg.token ? { defaults: { token: agent.reg.token } } : {}),
62
+ };
63
+ result.reg = reg;
64
+
65
+ (['dev', 'prd'] as (keyof IFullConsulAgentConfig)[]).forEach((id) => {
66
+ if (agent[id]) {
67
+ const {
68
+ host,
69
+ port,
70
+ secure,
71
+ token,
72
+ dc,
73
+ } = agent[id] as IConsulAgentConfig;
74
+ const agentOpts: IConsulAgentOptions = {
75
+ host: String(host || reg.host),
76
+ port: String(port || reg.port),
77
+ secure: parseBoolean(secure == null ? reg.secure : secure),
78
+ ...(token ? { defaults: { token } } : (reg.defaults ? { defaults: reg.defaults } : {})),
79
+ ...(dc ? { dc } : {}),
80
+ };
81
+ result[id] = agentOpts;
82
+ } else {
83
+ result[id] = { ...reg };
84
+ }
85
+ });
86
+ consulConfigTypes.forEach((id) => {
87
+ if (!Number(result[id].port)) {
88
+ throw new Error(`The port for consul agent[${id}] is invalid: [${result[id].port}]`);
89
+ }
90
+ // @ts-ignore
91
+ result[id][agentTypeS] = id;
92
+ });
93
+ return result;
94
+ };
95
+
96
+ export const prepareConsulAPI = async (clOptions: ICLOptions): Promise<IConsulAPI> => {
97
+ let logger = (clOptions.logger || loggerStub) as ILogger;
98
+ if (!logger?.info) {
99
+ logger = loggerStub;
100
+ }
101
+ const fullConsulAgentOptions: IFullConsulAgentOptions = await getConsulAgentOptions(clOptions);
102
+ if (dbg.on) {
103
+ debug(`CONSUL AGENT OPTIONS:\n${JSON.stringify(fullConsulAgentOptions, undefined, 2)}`);
104
+ }
105
+
106
+ const consulInstances = {} as { reg: ConsulClient, dev: ConsulClient, prd: ConsulClient };
107
+ consulConfigTypes.forEach((id) => {
108
+ const consulAgentOptions = fullConsulAgentOptions[id];
109
+ const consulInstance = new ConsulClient(consulAgentOptions);
110
+ // @ts-ignore
111
+ consulInstance[agentTypeS] = id;
112
+
113
+ consulInstances[id] = consulInstance;
114
+
115
+ consulInstance.onRequest((request: RequestInfo) => {
116
+ if (dbg.on) {
117
+ const msg = dbg.curl ? getCurlText(request) : getHttpRequestText(request);
118
+ debug(`[${request.id}] ${yellow}${msg}${reset}`);
119
+ }
120
+ });
121
+
122
+ consulInstance.onResponse((request: RequestInfo, response: ResponseInfo) => {
123
+ const rqId = `[${request.id}] `;
124
+ try {
125
+ const { statusCode = 0, body = null } = response || {};
126
+ debug(`${rqId}HTTP Status: ${statusCode}`);
127
+ if (statusCode > 299) {
128
+ if (body) {
129
+ logger.error(`${rqId}[CONSUL] ERROR: ${JSON.stringify(body)}`);
130
+ } else {
131
+ debug(`${rqId}response.body not found!`);
132
+ }
133
+ }
134
+ } catch (err: Error | any) {
135
+ logger.error(`ERROR (onResponse ${rqId}): \n err.message: ${err.message}\n err.stack:\n${err.stack}\n`);
136
+ }
137
+ });
138
+ });
139
+
140
+ const getAgentTypeByServiceID = (serviceId: string): keyof IFullConsulAgentConfig => {
141
+ const agentType = serviceId.substring(0, 3);
142
+ return (/(dev|prd)/.test(agentType) ? agentType : 'reg') as keyof IFullConsulAgentConfig;
143
+ };
144
+ const getAgentOptionsByServiceID = (serviceId: string): IConsulAgentOptions => fullConsulAgentOptions[getAgentTypeByServiceID(serviceId)];
145
+ const getConsulInstanceByServiceID = (serviceId: string): ConsulClient => consulInstances[getAgentTypeByServiceID(serviceId)];
146
+
147
+ const createConsulClientFromOptions = (agentOptions: IConsulAgentOptions): ConsulClient => new ConsulClient(agentOptions);
148
+
149
+ const api = {
150
+ // Returns the services the agent is managing. - список сервисов на этом агенте
151
+ async agentServiceList (apiArgs: IAPIArgs = {}) {
152
+ // ### GET http://<*.host>:<*.port>/v1/agent/services
153
+ let client: ConsulClient;
154
+ if (apiArgs.agentOptions) {
155
+ client = createConsulClientFromOptions(apiArgs.agentOptions);
156
+ } else if (apiArgs.agentType) {
157
+ client = consulInstances[apiArgs.agentType];
158
+ } else {
159
+ client = consulInstances.reg;
160
+ }
161
+ try {
162
+ const result = await client.agent.serviceList();
163
+ return result;
164
+ } catch (err: Error | any) {
165
+ logger.error(`[consul.agent.serviceList] ERROR:\n err.message: ${err.message}\n err.stack:\n${err.stack}\n`);
166
+ return apiArgs.withError ? err : false;
167
+ }
168
+ },
169
+
170
+ // Lists services in a given datacenter
171
+ async catalogServiceList (dc: string, apiArgs: IAPIArgs = {}): Promise<{ [serviceId: string]: string[] }> {
172
+ // ### GET https://<context.host>:<context.port>/v1/catalog/services?dc=<dc>
173
+ let client: ConsulClient;
174
+ if (apiArgs.agentOptions) {
175
+ client = createConsulClientFromOptions(apiArgs.agentOptions);
176
+ } else if (apiArgs.agentType) {
177
+ client = consulInstances[apiArgs.agentType];
178
+ } else {
179
+ const agentType = (Object.entries(fullConsulAgentOptions)
180
+ .find(([, v]) => v.dc === dc) || ['dev'])[0] as keyof IFullConsulAgentConfig;
181
+ client = consulInstances[agentType];
182
+ }
183
+ try {
184
+ const result = await client.catalog.serviceList(dc);
185
+ return result;
186
+ } catch (err: Error | any) {
187
+ logger.error(`[consul.catalog.serviceList] ERROR:\n err.message: ${err.message}\n err.stack:\n${err.stack}\n`);
188
+ return apiArgs.withError ? err : {};
189
+ }
190
+ },
191
+
192
+ // Returns the nodes and health info of a service
193
+ async consulHealthService (apiArgs: IAPIArgs): Promise<IConsulHealthServiceInfo[]> {
194
+ // ### GET https://<context.host>:<context.port>/v1/health/service/<apiArgs.options.serviceId>?passing=true&dc=<apiArgs.options.dc || context.dc>
195
+ const {
196
+ service: serviceId,
197
+ dc,
198
+ passing,
199
+ } = apiArgs.options;
200
+
201
+ let dcToUse = dc;
202
+ if (!dcToUse) {
203
+ const agentOptions = getAgentOptionsByServiceID(serviceId);
204
+ dcToUse = agentOptions.dc || undefined;
205
+ }
206
+
207
+ let client: ConsulClient;
208
+ if (apiArgs.agentOptions) {
209
+ client = createConsulClientFromOptions(apiArgs.agentOptions);
210
+ } else if (apiArgs.agentType) {
211
+ client = consulInstances[apiArgs.agentType];
212
+ } else {
213
+ client = getConsulInstanceByServiceID(serviceId);
214
+ }
215
+
216
+ try {
217
+ const result = await client.health.service({
218
+ service: serviceId,
219
+ dc: dcToUse,
220
+ passing,
221
+ });
222
+ return result as IConsulHealthServiceInfo[];
223
+ } catch (err: Error | any) {
224
+ logger.error(`[consul.health.service] ERROR:\n err.message: ${err.message}\n err.stack:\n${err.stack}\n`);
225
+ return apiArgs.withError ? err : [];
226
+ }
227
+ },
228
+
229
+ async getServiceInfo (serviceName: string): Promise<IConsulServiceInfo | undefined> {
230
+ // ### GET https://<context.host>:<context.port>/v1/health/service/<apiArgs.options.serviceId>?passing=true&dc=<apiArgs.options.dc || context.dc>
231
+ const result = await this.consulHealthService({
232
+ options: {
233
+ service: serviceName,
234
+ passing: true,
235
+ },
236
+ });
237
+ const res = result?.[0]?.Service;
238
+ if (!res) {
239
+ logger.debug(`No info about service ID ${cyan}${serviceName}`);
240
+ }
241
+ return res;
242
+ },
243
+
244
+ async getServiceSocket (serviceName: string, defaults: ISocketInfo): Promise<ISocketInfo> {
245
+ if (process.env.USE_DEFAULT_SERVICE_SOCKET) {
246
+ return defaults;
247
+ }
248
+ // В функции consulHealthService используется агент dev/prd в зависимости от префикса
249
+ const result: IConsulHealthServiceInfo[] = await this.consulHealthService({
250
+ options: {
251
+ service: serviceName,
252
+ passing: true,
253
+ },
254
+ });
255
+ if (!result || !result.length) {
256
+ logger.warn(`CONSUL: No working service found: ${cyan}${serviceName}${reset}. Return defaults ${defaults.host}:${defaults.port}`);
257
+ return defaults;
258
+ }
259
+
260
+ const service = result[0]?.Service;
261
+ const nodeAddress = result[0]?.Node?.Node;
262
+ const Address = service?.Address || nodeAddress;
263
+ const Port = service?.Port;
264
+
265
+ const host = await getFQDNCached(Address);
266
+ return {
267
+ host: host || Address || '',
268
+ port: Port || 0,
269
+ };
270
+ },
271
+
272
+ // Registers a new service.
273
+ async agentServiceRegister (options: IRegisterConfig, withError: boolean = false): Promise<boolean> {
274
+ // ### PUT http://<reg.host>:<reg.port>/v1/agent/service/register
275
+ try {
276
+ await consulInstances.reg.agent.serviceRegister(options);
277
+ return true;
278
+ } catch (err: Error | any) {
279
+ logger.error(`[consul.agent.service.register] ERROR:\n err.message: ${err.message}\n err.stack:\n${err.stack}\n`);
280
+ return withError ? err : false;
281
+ }
282
+ },
283
+
284
+ // Deregister a service.
285
+ async agentServiceDeregister (serviceId: string, apiArgs: IAPIArgs = {}): Promise<boolean> {
286
+ // ### PUT http://<reg.host>:<reg.port>/v1/agent/service/deregister/<serviceId>
287
+ let client: ConsulClient;
288
+ if (apiArgs.agentOptions) {
289
+ client = createConsulClientFromOptions(apiArgs.agentOptions);
290
+ } else if (apiArgs.agentType) {
291
+ client = consulInstances[apiArgs.agentType];
292
+ } else {
293
+ client = consulInstances.reg;
294
+ }
295
+ try {
296
+ await client.agent.serviceDeregister(serviceId);
297
+ return true;
298
+ } catch (err: Error | any) {
299
+ logger.error(`[consul.agent.service.deregister] ERROR:\n err.message: ${err.message}\n err.stack:\n${err.stack}\n`);
300
+ return apiArgs.withError ? err : false;
301
+ }
302
+ },
303
+
304
+ async deregisterIfNeed (serviceId: string, agentOptions?: IConsulAgentOptions): Promise<boolean> {
305
+ const apiArgs: IAPIArgs = agentOptions ? { agentOptions } : {};
306
+ const healthServiceInfo = await this.checkIfServiceRegistered(serviceId, apiArgs);
307
+ if (healthServiceInfo) {
308
+ const nodeHost = (healthServiceInfo.Node?.Node || '').toLowerCase()
309
+ .split('.')[0] || '';
310
+ const [agentType = 'reg'] = Object.entries(fullConsulAgentOptions)
311
+ .find(([, aOpt]) => aOpt.host.toLowerCase()
312
+ .startsWith(nodeHost)) || [];
313
+ apiArgs.agentType = agentType as keyof IFullConsulAgentConfig;
314
+ const isDeregister = await this.agentServiceDeregister(serviceId, apiArgs);
315
+
316
+ const agentHost = fullConsulAgentOptions[agentType as keyof IFullConsulAgentConfig].host;
317
+ const m = (wasnt: string = '') => `Previous registration of service '${cyan}${serviceId}${reset}'${wasnt} removed from consul agent ${blue}${agentHost}${reset}`;
318
+ if (isDeregister) {
319
+ logger.info(m());
320
+ } else {
321
+ logger.error(m(' was NOT'));
322
+ return false;
323
+ }
324
+ } else {
325
+ logger.info(`Service '${cyan}${serviceId}${reset}' is not registered in Consul`);
326
+ }
327
+ return true;
328
+ },
329
+
330
+ // Returns the members as seen by the consul agent. - список агентов (нод)
331
+ agentMembers: async (apiArgs: IAPIArgs = {}) => {
332
+ // ### GET http://<reg.host>:<reg.port>/v1/agent/members
333
+ let client: ConsulClient;
334
+ if (apiArgs.agentOptions) {
335
+ client = createConsulClientFromOptions(apiArgs.agentOptions);
336
+ } else if (apiArgs.agentType) {
337
+ client = consulInstances[apiArgs.agentType];
338
+ } else {
339
+ client = consulInstances.reg;
340
+ }
341
+ try {
342
+ const result = await client.agent.members();
343
+ return result;
344
+ } catch (err: Error | any) {
345
+ logger.error(`[consul.agent.members] ERROR:\n err.message: ${err.message}\n err.stack:\n${err.stack}\n`);
346
+ return apiArgs.withError ? err : false;
347
+ }
348
+ },
349
+
350
+ async checkIfServiceRegistered (serviceIdOrName: string, apiArgs: IAPIArgs = {}): Promise<IConsulHealthServiceInfo | undefined> {
351
+ if (!apiArgs.agentOptions && !apiArgs.agentType) {
352
+ apiArgs.agentType = getAgentTypeByServiceID(serviceIdOrName);
353
+ }
354
+ const result = await this.consulHealthService({ ...apiArgs, options: { service: serviceIdOrName } });
355
+ return result?.[0];
356
+ },
357
+
358
+ async registerService (registerConfig: IRegisterConfig, registerOptions: IRegisterOptions): Promise<TRegisterResult> {
359
+ const serviceId = registerConfig.id || registerConfig.name;
360
+ const srv = `Service '${cyan}${serviceId}${reset}'`;
361
+
362
+ const serviceInfo = await this.getServiceInfo(serviceId);
363
+ const diff = serviceConfigDiff(registerConfig, serviceInfo);
364
+ const isAlreadyRegistered = !!serviceInfo;
365
+
366
+ const already = (): TRegisterResult => {
367
+ if (!registerOptions.noAlreadyRegisteredMessage) {
368
+ if (dbg.on) {
369
+ console.log(`${srv} already registered in Consul`);
370
+ }
371
+ }
372
+ return 'already';
373
+ };
374
+
375
+ switch (registerOptions.registerType) {
376
+ case 'if-config-differ': {
377
+ if (!diff.length) {
378
+ return already();
379
+ }
380
+ logger.info(`${srv}. Configuration difference detected. New: config.${diff[0]}=${diff[1]} / Current: config.${diff[2]}=${diff[3]}`);
381
+ break;
382
+ }
383
+ case 'if-not-registered': {
384
+ if (isAlreadyRegistered) {
385
+ return already();
386
+ }
387
+ break;
388
+ }
389
+ }
390
+
391
+ if (isAlreadyRegistered && registerOptions.deleteOtherInstance) {
392
+ if (await this.agentServiceDeregister(serviceId)) {
393
+ logger.info(`Previous registration of ${srv} removed from Consul`);
394
+ }
395
+ }
396
+ const isJustRegistered = await this.agentServiceRegister(registerConfig);
397
+ if (isJustRegistered) {
398
+ if (dbg.on) {
399
+ logger.info(`${srv} is registered in Consul`);
400
+ }
401
+ } else {
402
+ logger.error(`${srv} is NOT registered in Consul`);
403
+ }
404
+ return isJustRegistered ? 'just' : false;
405
+ },
406
+ agentOptions: fullConsulAgentOptions,
407
+ getConsulAgentOptions,
408
+ };
409
+ return api;
410
+ };
411
+
412
+ const consulApiCache: ICache<IConsulAPI> = {};
413
+
414
+ export const getConsulApiCached = async (clOptions: ICLOptions): Promise<IConsulAPI> => mutex
415
+ .runExclusive<IConsulAPI>(async () => {
416
+ const hash = getConfigHash(clOptions);
417
+ if (!consulApiCache[hash]) {
418
+ minimizeCache(consulApiCache, MAX_API_CACHED);
419
+ const value = await prepareConsulAPI(clOptions);
420
+ consulApiCache[hash] = {
421
+ created: Date.now(),
422
+ value,
423
+ };
424
+ }
425
+ return consulApiCache[hash].value;
426
+ });