245 lines
7.5 KiB
JavaScript
245 lines
7.5 KiB
JavaScript
#!/usr/bin/node
|
|
|
|
const fsp = require('fs').promises;
|
|
const http = require('http');
|
|
const https = require('https');
|
|
|
|
class AntiEtcdCli
|
|
{
|
|
static parse(args)
|
|
{
|
|
const cmd = [];
|
|
const options = {};
|
|
for (let i = 2; i < args.length; i++)
|
|
{
|
|
const arg = args[i].toLowerCase().replace(/^--(.+)$/, (m, m1) => '--'+m1.replace(/-/g, '_'));
|
|
if (arg === '-h' || arg === '--help')
|
|
{
|
|
process.stderr.write(
|
|
'USAGE:\n'+
|
|
' anticli.js [OPTIONS] put <key> [<value>]\n'+
|
|
' anticli.js [OPTIONS] get <key> [-p|--prefix] [-v|--print-value-only] [-k|--keys-only]\n'+
|
|
' anticli.js [OPTIONS] del <key> [-p|--prefix]\n'+
|
|
'OPTIONS:\n'+
|
|
' [--endpoints|-e http://node1:2379,http://node2:2379,http://node3:2379]\n'+
|
|
' [--timeout 1000]'
|
|
);
|
|
process.exit();
|
|
}
|
|
else if (arg == '-e' || arg == '--endpoints')
|
|
{
|
|
options['endpoints'] = args[++i].split(/\s*[,\s]+\s*/);
|
|
}
|
|
else if (arg == '-p' || arg == '--prefix')
|
|
{
|
|
options['prefix'] = true;
|
|
}
|
|
else if (arg == '-v' || arg == '--print_value_only')
|
|
{
|
|
options['print_value_only'] = true;
|
|
}
|
|
else if (arg == '-k' || arg == '--keys_only')
|
|
{
|
|
options['keys_only'] = true;
|
|
}
|
|
else if (arg[0] == '-' && arg[1] !== '-')
|
|
{
|
|
process.stderr.write('Unknown option '+arg);
|
|
process.exit(1);
|
|
}
|
|
else if (arg.substr(0, 2) == '--')
|
|
{
|
|
options[arg.substr(2)] = args[++i];
|
|
}
|
|
else
|
|
{
|
|
cmd.push(arg);
|
|
}
|
|
}
|
|
if (!cmd.length || cmd[0] != 'get' && cmd[0] != 'put' && cmd[0] != 'del')
|
|
{
|
|
process.stderr.write('Supported commands: get, put, del. Use --help to see details\n');
|
|
process.exit(1);
|
|
}
|
|
return [ cmd, options ];
|
|
}
|
|
|
|
async run(cmd, options)
|
|
{
|
|
this.options = options;
|
|
if (!this.options.endpoints)
|
|
{
|
|
this.options.endpoints = [ 'http://localhost:2379' ];
|
|
}
|
|
if (cmd[0] == 'get')
|
|
{
|
|
await this.get(cmd.slice(1));
|
|
}
|
|
else if (cmd[0] == 'put')
|
|
{
|
|
await this.put(cmd[1], cmd.length > 2 ? cmd[2] : undefined);
|
|
}
|
|
else if (cmd[0] == 'del')
|
|
{
|
|
await this.del(cmd.slice(1));
|
|
}
|
|
// wait until output is fully flushed
|
|
await new Promise(ok => process.stdout.write('', ok));
|
|
await new Promise(ok => process.stderr.write('', ok));
|
|
process.exit(0);
|
|
}
|
|
|
|
async get(keys)
|
|
{
|
|
if (this.options.prefix)
|
|
{
|
|
keys = keys.map(k => k.replace(/\/+$/, ''));
|
|
}
|
|
const txn = { success: keys.map(key => ({ request_range: this.options.prefix ? { key: b64(key+'/'), range_end: b64(key+'0') } : { key: b64(key) } })) };
|
|
const res = await this.request('/v3/kv/txn', txn);
|
|
for (const r of res.responses||[])
|
|
{
|
|
if (r.response_range)
|
|
{
|
|
for (const kv of r.response_range.kvs)
|
|
{
|
|
if (!this.options.print_value_only)
|
|
{
|
|
process.stdout.write(de64(kv.key)+'\n');
|
|
}
|
|
if (!this.options.keys_only)
|
|
{
|
|
process.stdout.write(de64(kv.value)+'\n');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async put(key, value)
|
|
{
|
|
if (value === undefined)
|
|
{
|
|
value = await fsp.readFile(0, { encoding: 'utf-8' });
|
|
}
|
|
const res = await this.request('/v3/kv/put', { key: b64(key), value: b64(value) });
|
|
if (res.header)
|
|
{
|
|
process.stdout.write('OK\n');
|
|
}
|
|
}
|
|
|
|
async del(keys)
|
|
{
|
|
if (this.options.prefix)
|
|
{
|
|
keys = keys.map(k => k.replace(/\/+$/, ''));
|
|
}
|
|
const txn = { success: keys.map(key => ({ request_delete_range: this.options.prefix ? { key: b64(key+'/'), range_end: b64(key+'0') } : { key: b64(key) } })) };
|
|
const res = await this.request('/v3/kv/txn', txn);
|
|
for (const r of res.responses||[])
|
|
{
|
|
if (r.response_delete_range)
|
|
{
|
|
process.stdout.write(r.response_delete_range.deleted+'\n');
|
|
}
|
|
}
|
|
}
|
|
|
|
async request(path, body)
|
|
{
|
|
for (const url of this.options.endpoints)
|
|
{
|
|
const cur_url = url.replace(/\/+$/, '')+path;
|
|
const res = await POST(cur_url, body, this.options.timeout||1000);
|
|
if (res.json)
|
|
{
|
|
if (res.json.error)
|
|
{
|
|
process.stderr.write(cur_url+': '+res.json.error);
|
|
process.exit(1);
|
|
}
|
|
return res.json;
|
|
}
|
|
if (res.body)
|
|
{
|
|
process.stderr.write(cur_url+': '+res.body);
|
|
}
|
|
if (res.error)
|
|
{
|
|
process.stderr.write(cur_url+': '+res.error);
|
|
if (!res.response || !res.response.statusCode)
|
|
{
|
|
// This URL is unavailable
|
|
continue;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function POST(url, body, timeout)
|
|
{
|
|
return new Promise(ok =>
|
|
{
|
|
const body_text = Buffer.from(JSON.stringify(body));
|
|
let timer_id = timeout > 0 ? setTimeout(() =>
|
|
{
|
|
if (req)
|
|
req.abort();
|
|
req = null;
|
|
ok({ error: 'timeout' });
|
|
}, timeout) : null;
|
|
let req = (url.substr(0, 6).toLowerCase() == 'https://' ? https : http).request(url, { method: 'POST', headers: {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': body_text.length,
|
|
} }, (res) =>
|
|
{
|
|
if (!req)
|
|
{
|
|
return;
|
|
}
|
|
clearTimeout(timer_id);
|
|
let res_body = '';
|
|
res.setEncoding('utf8');
|
|
res.on('error', (error) => ok({ error }));
|
|
res.on('data', chunk => { res_body += chunk; });
|
|
res.on('end', () =>
|
|
{
|
|
if (res.statusCode != 200 || !/application\/json/i.exec(res.headers['content-type']))
|
|
{
|
|
ok({ response: res, body: res_body, code: res.statusCode });
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
res_body = JSON.parse(res_body);
|
|
ok({ response: res, json: res_body });
|
|
}
|
|
catch (e)
|
|
{
|
|
ok({ response: res, error: e, body: res_body });
|
|
}
|
|
});
|
|
});
|
|
req.on('error', (error) => ok({ error }));
|
|
req.on('close', () => ok({ error: new Error('Connection closed prematurely') }));
|
|
req.write(body_text);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
function b64(str)
|
|
{
|
|
return Buffer.from(str).toString('base64');
|
|
}
|
|
|
|
function de64(str)
|
|
{
|
|
return Buffer.from(str, 'base64').toString();
|
|
}
|
|
|
|
new AntiEtcdCli().run(...AntiEtcdCli.parse(process.argv)).catch(console.error);
|