antietcd 1.0.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/README.md +501 -0
- package/anticli.js +263 -0
- package/anticluster.js +526 -0
- package/antietcd-app.js +122 -0
- package/antietcd.d.ts +155 -0
- package/antietcd.js +552 -0
- package/antipersistence.js +138 -0
- package/common.js +38 -0
- package/etctree.js +875 -0
- package/package.json +55 -0
- package/stable-stringify.js +78 -0
package/anticli.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// CLI for AntiEtcd
|
|
4
|
+
// (c) Vitaliy Filippov, 2024
|
|
5
|
+
// License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1
|
|
6
|
+
|
|
7
|
+
const fsp = require('fs').promises;
|
|
8
|
+
const http = require('http');
|
|
9
|
+
const https = require('https');
|
|
10
|
+
|
|
11
|
+
const help_text = `CLI for AntiEtcd
|
|
12
|
+
(c) Vitaliy Filippov, 2024
|
|
13
|
+
License: Mozilla Public License 2.0 or Vitastor Network Public License 1.1
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
|
|
17
|
+
anticli.js [OPTIONS] put <key> [<value>]
|
|
18
|
+
anticli.js [OPTIONS] get <key> [-p|--prefix] [-v|--print-value-only] [-k|--keys-only]
|
|
19
|
+
anticli.js [OPTIONS] del <key> [-p|--prefix]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
|
|
23
|
+
[--endpoints|-e http://node1:2379,http://node2:2379,http://node3:2379]
|
|
24
|
+
[--cert cert.pem] [--key key.pem] [--timeout 1000]
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
class AntiEtcdCli
|
|
28
|
+
{
|
|
29
|
+
static parse(args)
|
|
30
|
+
{
|
|
31
|
+
const cmd = [];
|
|
32
|
+
const options = {};
|
|
33
|
+
for (let i = 2; i < args.length; i++)
|
|
34
|
+
{
|
|
35
|
+
const arg = args[i].toLowerCase().replace(/^--(.+)$/, (m, m1) => '--'+m1.replace(/-/g, '_'));
|
|
36
|
+
if (arg === '-h' || arg === '--help')
|
|
37
|
+
{
|
|
38
|
+
process.stderr.write(help_text);
|
|
39
|
+
process.exit();
|
|
40
|
+
}
|
|
41
|
+
else if (arg == '-e' || arg == '--endpoints')
|
|
42
|
+
{
|
|
43
|
+
options['endpoints'] = args[++i].split(/\s*[,\s]+\s*/);
|
|
44
|
+
}
|
|
45
|
+
else if (arg == '-p' || arg == '--prefix')
|
|
46
|
+
{
|
|
47
|
+
options['prefix'] = true;
|
|
48
|
+
}
|
|
49
|
+
else if (arg == '-v' || arg == '--print_value_only')
|
|
50
|
+
{
|
|
51
|
+
options['print_value_only'] = true;
|
|
52
|
+
}
|
|
53
|
+
else if (arg == '-k' || arg == '--keys_only')
|
|
54
|
+
{
|
|
55
|
+
options['keys_only'] = true;
|
|
56
|
+
}
|
|
57
|
+
else if (arg[0] == '-' && arg[1] !== '-')
|
|
58
|
+
{
|
|
59
|
+
process.stderr.write('Unknown option '+arg);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
else if (arg.substr(0, 2) == '--')
|
|
63
|
+
{
|
|
64
|
+
options[arg.substr(2)] = args[++i];
|
|
65
|
+
}
|
|
66
|
+
else
|
|
67
|
+
{
|
|
68
|
+
cmd.push(arg);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!cmd.length || cmd[0] != 'get' && cmd[0] != 'put' && cmd[0] != 'del')
|
|
72
|
+
{
|
|
73
|
+
process.stderr.write('Supported commands: get, put, del. Use --help to see details\n');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
return [ cmd, options ];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async run(cmd, options)
|
|
80
|
+
{
|
|
81
|
+
this.options = options;
|
|
82
|
+
if (!this.options.endpoints)
|
|
83
|
+
{
|
|
84
|
+
this.options.endpoints = [ 'http://localhost:2379' ];
|
|
85
|
+
}
|
|
86
|
+
if (this.options.cert && this.options.key)
|
|
87
|
+
{
|
|
88
|
+
this.tls = {
|
|
89
|
+
key: await fsp.readFile(this.options.key),
|
|
90
|
+
cert: await fsp.readFile(this.options.cert),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (cmd[0] == 'get')
|
|
94
|
+
{
|
|
95
|
+
await this.get(cmd.slice(1));
|
|
96
|
+
}
|
|
97
|
+
else if (cmd[0] == 'put')
|
|
98
|
+
{
|
|
99
|
+
await this.put(cmd[1], cmd.length > 2 ? cmd[2] : undefined);
|
|
100
|
+
}
|
|
101
|
+
else if (cmd[0] == 'del')
|
|
102
|
+
{
|
|
103
|
+
await this.del(cmd.slice(1));
|
|
104
|
+
}
|
|
105
|
+
// wait until output is fully flushed
|
|
106
|
+
await new Promise(ok => process.stdout.write('', ok));
|
|
107
|
+
await new Promise(ok => process.stderr.write('', ok));
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async get(keys)
|
|
112
|
+
{
|
|
113
|
+
if (this.options.prefix)
|
|
114
|
+
{
|
|
115
|
+
keys = keys.map(k => k.replace(/\/+$/, ''));
|
|
116
|
+
}
|
|
117
|
+
const txn = { success: keys.map(key => ({ request_range: this.options.prefix ? { key: b64(key+'/'), range_end: b64(key+'0') } : { key: b64(key) } })) };
|
|
118
|
+
const res = await this.request('/v3/kv/txn', txn);
|
|
119
|
+
for (const r of res.responses||[])
|
|
120
|
+
{
|
|
121
|
+
if (r.response_range)
|
|
122
|
+
{
|
|
123
|
+
for (const kv of r.response_range.kvs)
|
|
124
|
+
{
|
|
125
|
+
if (!this.options.print_value_only)
|
|
126
|
+
{
|
|
127
|
+
process.stdout.write(de64(kv.key)+'\n');
|
|
128
|
+
}
|
|
129
|
+
if (!this.options.keys_only)
|
|
130
|
+
{
|
|
131
|
+
process.stdout.write(de64(kv.value)+'\n');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async put(key, value)
|
|
139
|
+
{
|
|
140
|
+
if (value === undefined)
|
|
141
|
+
{
|
|
142
|
+
value = await fsp.readFile(0, { encoding: 'utf-8' });
|
|
143
|
+
}
|
|
144
|
+
const res = await this.request('/v3/kv/put', { key: b64(key), value: b64(value) });
|
|
145
|
+
if (res.header)
|
|
146
|
+
{
|
|
147
|
+
process.stdout.write('OK\n');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async del(keys)
|
|
152
|
+
{
|
|
153
|
+
if (this.options.prefix)
|
|
154
|
+
{
|
|
155
|
+
keys = keys.map(k => k.replace(/\/+$/, ''));
|
|
156
|
+
}
|
|
157
|
+
const txn = { success: keys.map(key => ({ request_delete_range: this.options.prefix ? { key: b64(key+'/'), range_end: b64(key+'0') } : { key: b64(key) } })) };
|
|
158
|
+
const res = await this.request('/v3/kv/txn', txn);
|
|
159
|
+
for (const r of res.responses||[])
|
|
160
|
+
{
|
|
161
|
+
if (r.response_delete_range)
|
|
162
|
+
{
|
|
163
|
+
process.stdout.write(r.response_delete_range.deleted+'\n');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async request(path, body)
|
|
169
|
+
{
|
|
170
|
+
for (const url of this.options.endpoints)
|
|
171
|
+
{
|
|
172
|
+
const cur_url = url.replace(/\/+$/, '')+path;
|
|
173
|
+
const res = await POST(cur_url, this.tls||{}, body, this.options.timeout||1000);
|
|
174
|
+
if (res.json)
|
|
175
|
+
{
|
|
176
|
+
if (res.json.error)
|
|
177
|
+
{
|
|
178
|
+
process.stderr.write(cur_url+': '+res.json.error);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
return res.json;
|
|
182
|
+
}
|
|
183
|
+
if (res.body)
|
|
184
|
+
{
|
|
185
|
+
process.stderr.write(cur_url+': '+res.body);
|
|
186
|
+
}
|
|
187
|
+
if (res.error)
|
|
188
|
+
{
|
|
189
|
+
process.stderr.write(cur_url+': '+res.error);
|
|
190
|
+
if (!res.response || !res.response.statusCode)
|
|
191
|
+
{
|
|
192
|
+
// This URL is unavailable
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function POST(url, options, body, timeout)
|
|
203
|
+
{
|
|
204
|
+
return new Promise(ok =>
|
|
205
|
+
{
|
|
206
|
+
const body_text = Buffer.from(JSON.stringify(body));
|
|
207
|
+
let timer_id = timeout > 0 ? setTimeout(() =>
|
|
208
|
+
{
|
|
209
|
+
if (req)
|
|
210
|
+
req.abort();
|
|
211
|
+
req = null;
|
|
212
|
+
ok({ error: 'timeout' });
|
|
213
|
+
}, timeout) : null;
|
|
214
|
+
let req = (url.substr(0, 6).toLowerCase() == 'https://' ? https : http).request(url, { method: 'POST', headers: {
|
|
215
|
+
'Content-Type': 'application/json',
|
|
216
|
+
'Content-Length': body_text.length,
|
|
217
|
+
}, timeout, ...options }, (res) =>
|
|
218
|
+
{
|
|
219
|
+
if (!req)
|
|
220
|
+
{
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
clearTimeout(timer_id);
|
|
224
|
+
let res_body = '';
|
|
225
|
+
res.setEncoding('utf8');
|
|
226
|
+
res.on('error', (error) => ok({ error }));
|
|
227
|
+
res.on('data', chunk => { res_body += chunk; });
|
|
228
|
+
res.on('end', () =>
|
|
229
|
+
{
|
|
230
|
+
if (res.statusCode != 200 || !/application\/json/i.exec(res.headers['content-type']))
|
|
231
|
+
{
|
|
232
|
+
ok({ response: res, body: res_body, code: res.statusCode });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
try
|
|
236
|
+
{
|
|
237
|
+
res_body = JSON.parse(res_body);
|
|
238
|
+
ok({ response: res, json: res_body });
|
|
239
|
+
}
|
|
240
|
+
catch (e)
|
|
241
|
+
{
|
|
242
|
+
ok({ response: res, error: e, body: res_body });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
req.on('error', (error) => ok({ error }));
|
|
247
|
+
req.on('close', () => ok({ error: new Error('Connection closed prematurely') }));
|
|
248
|
+
req.write(body_text);
|
|
249
|
+
req.end();
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function b64(str)
|
|
254
|
+
{
|
|
255
|
+
return Buffer.from(str).toString('base64');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function de64(str)
|
|
259
|
+
{
|
|
260
|
+
return Buffer.from(str, 'base64').toString();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
new AntiEtcdCli().run(...AntiEtcdCli.parse(process.argv)).catch(console.error);
|