zgrzyt 2.2.0 → 2.2.2
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/index.js +1 -1
- package/lib/check.js +28 -30
- package/lib/cloudflare.js +70 -38
- package/lib/config.js +7 -12
- package/lib/dns.js +4 -1
- package/lib/report.js +15 -8
- package/lib/state.js +3 -9
- package/lib/zgrzyt.js +4 -1
- package/package.json +3 -4
package/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import rc from 'rc';
|
|
2
2
|
import prepareConfig from './lib/config.js';
|
|
3
|
-
import { zgrzyt } from './lib/zgrzyt.js';
|
|
4
3
|
import { report } from './lib/report.js';
|
|
5
4
|
import { onExit } from './lib/state.js';
|
|
5
|
+
import { zgrzyt } from './lib/zgrzyt.js';
|
|
6
6
|
|
|
7
7
|
const conf = rc('zgrzyt');
|
|
8
8
|
const apis = prepareConfig(conf);
|
package/lib/check.js
CHANGED
|
@@ -18,15 +18,17 @@ const USER_AGENT = `${name}/${version} (${homepage})`;
|
|
|
18
18
|
|
|
19
19
|
/* global URL */
|
|
20
20
|
|
|
21
|
-
export {
|
|
22
|
-
checkServices
|
|
23
|
-
};
|
|
21
|
+
export { checkServices };
|
|
24
22
|
|
|
25
23
|
async function checkService(server, api) {
|
|
26
24
|
const { ipv4 = true, ipv6 = false } = api;
|
|
27
25
|
const addresses = await resolve(server, { ipv4, ipv6 });
|
|
28
26
|
if (debug.enabled) {
|
|
29
|
-
debug(
|
|
27
|
+
debug(
|
|
28
|
+
'Resolved %s to %s',
|
|
29
|
+
server,
|
|
30
|
+
addresses.map(a => a.address).join(', ')
|
|
31
|
+
);
|
|
30
32
|
}
|
|
31
33
|
const oks = await Promise.all(addresses.map(a => checkApi(api, a)));
|
|
32
34
|
// collate results
|
|
@@ -41,13 +43,7 @@ async function checkService(server, api) {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
async function checkApi(api, { address, family }) {
|
|
44
|
-
|
|
45
|
-
const {
|
|
46
|
-
url,
|
|
47
|
-
method = 'HEAD',
|
|
48
|
-
timeout = 5000,
|
|
49
|
-
headers = {}
|
|
50
|
-
} = api;
|
|
46
|
+
const { url, method = 'HEAD', timeout = 5000, headers = {} } = api;
|
|
51
47
|
|
|
52
48
|
debug('Checking %s on %s with timeout %dms', url, address, timeout);
|
|
53
49
|
|
|
@@ -67,26 +63,28 @@ async function checkApi(api, { address, family }) {
|
|
|
67
63
|
return result;
|
|
68
64
|
|
|
69
65
|
function makeRequest() {
|
|
70
|
-
return new Promise(resolve =>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.on('timeout', function () {
|
|
81
|
-
debug('Timeout for %s on %s', url, address);
|
|
82
|
-
this.destroy(new Error('Request timeout.'));
|
|
83
|
-
})
|
|
84
|
-
.on('response', res => resolve(isOk(res)))
|
|
85
|
-
.on('error', e => {
|
|
86
|
-
debug('request error:', e);
|
|
87
|
-
resolve(false);
|
|
66
|
+
return new Promise(resolve =>
|
|
67
|
+
request(url, {
|
|
68
|
+
method,
|
|
69
|
+
family,
|
|
70
|
+
lookup,
|
|
71
|
+
timeout,
|
|
72
|
+
headers: {
|
|
73
|
+
'User-Agent': USER_AGENT,
|
|
74
|
+
...headers
|
|
75
|
+
}
|
|
88
76
|
})
|
|
89
|
-
|
|
77
|
+
.on('timeout', function () {
|
|
78
|
+
debug('Timeout for %s on %s', url, address);
|
|
79
|
+
this.destroy(new Error('Request timeout.'));
|
|
80
|
+
})
|
|
81
|
+
.on('response', res => resolve(isOk(res)))
|
|
82
|
+
.on('error', e => {
|
|
83
|
+
debug('request error:', e);
|
|
84
|
+
resolve(false);
|
|
85
|
+
})
|
|
86
|
+
.end()
|
|
87
|
+
);
|
|
90
88
|
|
|
91
89
|
function isOk({ statusCode }) {
|
|
92
90
|
const ok = statusCode >= 200 && statusCode < 300;
|
package/lib/cloudflare.js
CHANGED
|
@@ -1,38 +1,74 @@
|
|
|
1
|
-
import got from 'got';
|
|
2
1
|
import makeDebug from 'debug';
|
|
3
2
|
|
|
4
3
|
const debug = makeDebug('zgrzyt:cloudflare');
|
|
5
4
|
|
|
6
5
|
export default client;
|
|
7
6
|
|
|
7
|
+
/* global AbortController, fetch, URLSearchParams */
|
|
8
|
+
|
|
8
9
|
class CloudflareError extends Error {
|
|
9
10
|
constructor(errors) {
|
|
10
11
|
super(`Cloudflare API error: ${errors[0].message}`);
|
|
11
12
|
}
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
function
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
function makeFetch({ token, timeout, retry }) {
|
|
16
|
+
const authorization = `Bearer ${token}`;
|
|
17
|
+
return {
|
|
18
|
+
put: (...args) => retryFetch('PUT', ...args),
|
|
19
|
+
get: (...args) => retryFetch('GET', ...args)
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
async function retryFetch(...args) {
|
|
23
|
+
while (true) {
|
|
24
|
+
try {
|
|
25
|
+
return await doFetch(...args);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (--retry <= 0) {
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
23
31
|
}
|
|
24
|
-
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function doFetch(method, command, { searchParams, json }) {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
37
|
+
const url = new URL(command, 'https://api.cloudflare.com/client/v4/');
|
|
38
|
+
if (searchParams) {
|
|
39
|
+
url.search = new URLSearchParams(searchParams);
|
|
40
|
+
}
|
|
41
|
+
const options = {
|
|
42
|
+
method,
|
|
43
|
+
headers: { authorization },
|
|
44
|
+
signal: controller.signal
|
|
45
|
+
};
|
|
46
|
+
if (json) {
|
|
47
|
+
options.headers['Content-Type'] = 'application/json';
|
|
48
|
+
options.body = JSON.stringify(json);
|
|
49
|
+
}
|
|
50
|
+
const res = await fetch(url, options);
|
|
51
|
+
clearTimeout(id);
|
|
52
|
+
return res.json();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function client({ token, timeout = 4000, retry = 2 }) {
|
|
57
|
+
const cf = makeFetch({ token, timeout, retry });
|
|
25
58
|
const cacheZones = Object.create(null);
|
|
26
59
|
let pZones;
|
|
60
|
+
return {
|
|
61
|
+
switchToService,
|
|
62
|
+
listRecords
|
|
63
|
+
};
|
|
27
64
|
|
|
28
65
|
async function switchToService(zoneName, domain, good) {
|
|
29
66
|
const zone = await getZone(zoneName);
|
|
30
67
|
const record = await getRecord(zone.id, domain);
|
|
31
68
|
if (record.type === 'CNAME') {
|
|
32
69
|
return updateRecord(zone.id, record.id, record.proxied, domain, good);
|
|
33
|
-
} else {
|
|
34
|
-
console.error('Cannot only update CNAME records');
|
|
35
70
|
}
|
|
71
|
+
console.error('Cannot only update CNAME records');
|
|
36
72
|
}
|
|
37
73
|
|
|
38
74
|
async function listRecords(zoneName, domain) {
|
|
@@ -63,12 +99,7 @@ function client({ token, timeout = 4000, retry = 2 }) {
|
|
|
63
99
|
|
|
64
100
|
async function getPage(page) {
|
|
65
101
|
debug('Getting page %d', page);
|
|
66
|
-
const {
|
|
67
|
-
success,
|
|
68
|
-
errors,
|
|
69
|
-
result,
|
|
70
|
-
result_info: { total_count }
|
|
71
|
-
} = await cf.get('zones', {
|
|
102
|
+
const { success, errors, result, result_info } = await cf.get('zones', {
|
|
72
103
|
searchParams: {
|
|
73
104
|
status: 'active',
|
|
74
105
|
page,
|
|
@@ -81,23 +112,26 @@ function client({ token, timeout = 4000, retry = 2 }) {
|
|
|
81
112
|
}
|
|
82
113
|
|
|
83
114
|
zones.push(...result);
|
|
84
|
-
return zones.length < total_count;
|
|
115
|
+
return zones.length < result_info.total_count;
|
|
85
116
|
}
|
|
86
117
|
|
|
87
118
|
debug('Listing zones...');
|
|
88
|
-
for (let page = 1; await getPage(page); page++) {
|
|
119
|
+
for (let page = 1; await getPage(page); page++) {}
|
|
89
120
|
debug('Listing zones done.');
|
|
90
121
|
|
|
91
122
|
return zones;
|
|
92
123
|
}
|
|
93
124
|
|
|
94
125
|
async function getRecord(zoneId, domain) {
|
|
95
|
-
const { success, result, errors } = await cf.get(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
126
|
+
const { success, result, errors } = await cf.get(
|
|
127
|
+
`zones/${zoneId}/dns_records`,
|
|
128
|
+
{
|
|
129
|
+
searchParams: {
|
|
130
|
+
name: domain,
|
|
131
|
+
type: 'CNAME'
|
|
132
|
+
}
|
|
99
133
|
}
|
|
100
|
-
|
|
134
|
+
);
|
|
101
135
|
|
|
102
136
|
if (!success) {
|
|
103
137
|
throw new CloudflareError(errors);
|
|
@@ -111,14 +145,17 @@ function client({ token, timeout = 4000, retry = 2 }) {
|
|
|
111
145
|
}
|
|
112
146
|
|
|
113
147
|
async function updateRecord(zoneId, recordId, proxied, domain, good) {
|
|
114
|
-
const { success, errors, result } = await cf.put(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
148
|
+
const { success, errors, result } = await cf.put(
|
|
149
|
+
`zones/${zoneId}/dns_records/${recordId}`,
|
|
150
|
+
{
|
|
151
|
+
json: {
|
|
152
|
+
name: domain,
|
|
153
|
+
proxied,
|
|
154
|
+
content: good.server,
|
|
155
|
+
type: 'CNAME'
|
|
156
|
+
}
|
|
120
157
|
}
|
|
121
|
-
|
|
158
|
+
);
|
|
122
159
|
|
|
123
160
|
debug('Update record', success, errors, result);
|
|
124
161
|
|
|
@@ -128,9 +165,4 @@ function client({ token, timeout = 4000, retry = 2 }) {
|
|
|
128
165
|
|
|
129
166
|
return success;
|
|
130
167
|
}
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
switchToService,
|
|
134
|
-
listRecords
|
|
135
|
-
};
|
|
136
168
|
}
|
package/lib/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fromUrl, parseDomain } from 'parse-domain';
|
|
2
2
|
import makeClient from './cloudflare.js';
|
|
3
3
|
|
|
4
4
|
export default prepareConfig;
|
|
@@ -15,21 +15,17 @@ function getDomainAndZone({ url, domain: apiDomain }) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function prepareConfig(config) {
|
|
18
|
-
const {
|
|
19
|
-
cloudflare,
|
|
20
|
-
api = {},
|
|
21
|
-
cluster = {}
|
|
22
|
-
} = config;
|
|
18
|
+
const { cloudflare, api = {}, cluster = {} } = config;
|
|
23
19
|
|
|
24
20
|
if (!cloudflare?.token) {
|
|
25
21
|
console.error('Cloudflare API token not configured');
|
|
26
22
|
return;
|
|
27
23
|
}
|
|
28
24
|
if (typeof cloudflare.timeout === 'string') {
|
|
29
|
-
cloudflare.timeout = parseInt(cloudflare.timeout);
|
|
25
|
+
cloudflare.timeout = Number.parseInt(cloudflare.timeout);
|
|
30
26
|
}
|
|
31
27
|
if (typeof cloudflare.retry === 'string') {
|
|
32
|
-
cloudflare.retry = parseInt(cloudflare.retry);
|
|
28
|
+
cloudflare.retry = Number.parseInt(cloudflare.retry);
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
// collect all APIs
|
|
@@ -49,10 +45,10 @@ function prepareConfig(config) {
|
|
|
49
45
|
}
|
|
50
46
|
|
|
51
47
|
const { domain, zone } = getDomainAndZone(api);
|
|
52
|
-
const timeout = parseInt(api.timeout || config.timeout, 10) || 250;
|
|
53
|
-
const retry = parseInt(api.retry || config.retry, 10) || 2;
|
|
48
|
+
const timeout = Number.parseInt(api.timeout || config.timeout, 10) || 250;
|
|
49
|
+
const retry = Number.parseInt(api.retry || config.retry, 10) || 2;
|
|
54
50
|
const force = 'force' in api ? api.force : config.force;
|
|
55
|
-
const repair = parseInt(api.repair || config.repair, 10) || 5;
|
|
51
|
+
const repair = Number.parseInt(api.repair || config.repair, 10) || 5;
|
|
56
52
|
const ipv6 = Boolean(api.ipv6 ?? config.ipv6);
|
|
57
53
|
const ipv4 = Boolean(api.ipv4 ?? config.ipv4 ?? true);
|
|
58
54
|
|
|
@@ -88,6 +84,5 @@ function prepareConfig(config) {
|
|
|
88
84
|
force,
|
|
89
85
|
repair
|
|
90
86
|
};
|
|
91
|
-
|
|
92
87
|
}
|
|
93
88
|
}
|
package/lib/dns.js
CHANGED
|
@@ -39,7 +39,10 @@ async function doResolve(domain) {
|
|
|
39
39
|
async function resolveWithFamily(rrtype) {
|
|
40
40
|
try {
|
|
41
41
|
const addresses = await resolver.resolve(domain, rrtype);
|
|
42
|
-
return addresses.map(address => ({
|
|
42
|
+
return addresses.map(address => ({
|
|
43
|
+
address,
|
|
44
|
+
family: RRTYPE_TO_FAMILY[rrtype]
|
|
45
|
+
}));
|
|
43
46
|
} catch (error) {
|
|
44
47
|
debug('Failed to resolve %s with type %s: %s', domain, rrtype, error);
|
|
45
48
|
return [];
|
package/lib/report.js
CHANGED
|
@@ -13,10 +13,21 @@ function collect(context, { url, domain, record, good, switched }) {
|
|
|
13
13
|
if (!good) {
|
|
14
14
|
context.missing.push(url);
|
|
15
15
|
} else if (switched) {
|
|
16
|
-
const line = sprintf(
|
|
16
|
+
const line = sprintf(
|
|
17
|
+
'%-25s %-25s => %-25s [%s]',
|
|
18
|
+
domain,
|
|
19
|
+
record.content,
|
|
20
|
+
good.server,
|
|
21
|
+
good.address
|
|
22
|
+
);
|
|
17
23
|
context.switched.push(line);
|
|
18
24
|
} else {
|
|
19
|
-
const line = sprintf(
|
|
25
|
+
const line = sprintf(
|
|
26
|
+
'%-25s %-25s [%s]',
|
|
27
|
+
record.name,
|
|
28
|
+
good.server,
|
|
29
|
+
good.address
|
|
30
|
+
);
|
|
20
31
|
context.noops.push(line);
|
|
21
32
|
}
|
|
22
33
|
return context;
|
|
@@ -28,11 +39,7 @@ function formatError(err) {
|
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
function reportAll(collected, errors) {
|
|
31
|
-
const {
|
|
32
|
-
missing,
|
|
33
|
-
noops,
|
|
34
|
-
switched
|
|
35
|
-
} = collected;
|
|
42
|
+
const { missing, noops, switched } = collected;
|
|
36
43
|
|
|
37
44
|
let exitCode = 0;
|
|
38
45
|
const lines = [];
|
|
@@ -49,7 +56,7 @@ function reportAll(collected, errors) {
|
|
|
49
56
|
lines.push('No changes:\n', ...noops, '\n');
|
|
50
57
|
}
|
|
51
58
|
if (errors.length) {
|
|
52
|
-
lines.push('Errors:\n', ...errors,
|
|
59
|
+
lines.push('Errors:\n', ...errors, '\n');
|
|
53
60
|
exitCode = -1;
|
|
54
61
|
}
|
|
55
62
|
|
package/lib/state.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import { readFile, writeFile
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
|
|
3
|
-
export {
|
|
4
|
-
updateHealth,
|
|
5
|
-
onExit
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
const {
|
|
9
|
-
ZGRZYT_STATE = '/var/lib/zgrzyt/state.json'
|
|
10
|
-
} = process.env;
|
|
3
|
+
export { updateHealth, onExit };
|
|
11
4
|
|
|
5
|
+
const { ZGRZYT_STATE = '/var/lib/zgrzyt/state.json' } = process.env;
|
|
12
6
|
|
|
13
7
|
const state_p = read();
|
|
14
8
|
let hook;
|
package/lib/zgrzyt.js
CHANGED
|
@@ -3,7 +3,10 @@ import { checkServices } from './check.js';
|
|
|
3
3
|
export async function zgrzyt({ servers, api, client, force, repair }) {
|
|
4
4
|
const { domain, zone } = api;
|
|
5
5
|
const record = await client.listRecords(zone, domain);
|
|
6
|
-
const good = await checkServices(servers, record.content, api, {
|
|
6
|
+
const good = await checkServices(servers, record.content, api, {
|
|
7
|
+
force,
|
|
8
|
+
repair
|
|
9
|
+
});
|
|
7
10
|
const result = {
|
|
8
11
|
url: api.url,
|
|
9
12
|
domain,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zgrzyt",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "Poor man's load balancing DNS switcher.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": {
|
|
@@ -27,13 +27,12 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"debug": "~4",
|
|
30
|
-
"got": "~14",
|
|
31
30
|
"parse-domain": "~8",
|
|
32
31
|
"rc": "^1.2.8",
|
|
33
32
|
"sprintfjs": "^1.2.16"
|
|
34
33
|
},
|
|
35
34
|
"devDependencies": {
|
|
36
|
-
"@
|
|
35
|
+
"@biomejs/biome": "^1.9.4"
|
|
37
36
|
},
|
|
38
37
|
"scripts": {
|
|
39
38
|
"test": "make check"
|
|
@@ -43,4 +42,4 @@
|
|
|
43
42
|
"lib",
|
|
44
43
|
"bin"
|
|
45
44
|
]
|
|
46
|
-
}
|
|
45
|
+
}
|