greybull 0.2.0 → 0.2.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/dist/index.js +111 -39
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -94,7 +94,7 @@ var ApiError = class extends Error {
|
|
|
94
94
|
this.status = status;
|
|
95
95
|
}
|
|
96
96
|
};
|
|
97
|
-
var VERSION = "0.2.
|
|
97
|
+
var VERSION = "0.2.1";
|
|
98
98
|
function buildRequest(opts) {
|
|
99
99
|
const url = new URL(opts.apiUrl.replace(/\/+$/, "") + opts.path);
|
|
100
100
|
if (opts.query) {
|
|
@@ -311,8 +311,35 @@ function page(title, body) {
|
|
|
311
311
|
import fs2 from "fs";
|
|
312
312
|
import os3 from "os";
|
|
313
313
|
import path2 from "path";
|
|
314
|
+
import readline2 from "readline/promises";
|
|
314
315
|
import { spawnSync } from "child_process";
|
|
315
|
-
var RECORD_TYPES = [
|
|
316
|
+
var RECORD_TYPES = [
|
|
317
|
+
"A",
|
|
318
|
+
"AAAA",
|
|
319
|
+
"ALIAS",
|
|
320
|
+
"CAA",
|
|
321
|
+
"CERT",
|
|
322
|
+
"CNAME",
|
|
323
|
+
"DNSKEY",
|
|
324
|
+
"DS",
|
|
325
|
+
"HINFO",
|
|
326
|
+
"HTTPS",
|
|
327
|
+
"LOC",
|
|
328
|
+
"MX",
|
|
329
|
+
"NAPTR",
|
|
330
|
+
"NS",
|
|
331
|
+
"OPENPGPKEY",
|
|
332
|
+
"PTR",
|
|
333
|
+
"RP",
|
|
334
|
+
"SMIMEA",
|
|
335
|
+
"SPF",
|
|
336
|
+
"SRV",
|
|
337
|
+
"SSHFP",
|
|
338
|
+
"SVCB",
|
|
339
|
+
"TLSA",
|
|
340
|
+
"TXT",
|
|
341
|
+
"URI"
|
|
342
|
+
];
|
|
316
343
|
function buildListRecords(apiUrl, zone, opts) {
|
|
317
344
|
return buildRequest({
|
|
318
345
|
apiUrl,
|
|
@@ -436,7 +463,6 @@ async function deleteCmd(ctx2, zone, type, name, content, opts) {
|
|
|
436
463
|
function runSpec(spec, ctx2) {
|
|
437
464
|
return performRequest(spec, ctx2.apiKey).catch((err) => fail(err, ctx2));
|
|
438
465
|
}
|
|
439
|
-
var ERR_PREFIX = "; !! ";
|
|
440
466
|
function renderZoneFile(zone, records) {
|
|
441
467
|
const rows = records.map((r) => {
|
|
442
468
|
const content = r.type === "MX" && r.priority !== void 0 ? `${r.priority} ${r.content}` : r.content;
|
|
@@ -446,6 +472,10 @@ function renderZoneFile(zone, records) {
|
|
|
446
472
|
const ttlW = Math.max(3, ...rows.map((r) => r.ttl.length));
|
|
447
473
|
const typeW = Math.max(4, ...rows.map((r) => r.type.length));
|
|
448
474
|
const header = [
|
|
475
|
+
// Editor mode hints (comments, so the parser ignores them): Emacs dns-mode
|
|
476
|
+
// on line 1, Vim bindzone via the modeline at the end. Together with the
|
|
477
|
+
// .zone temp-file extension these give BIND zone syntax highlighting.
|
|
478
|
+
"; -*- mode: dns -*-",
|
|
449
479
|
`; Zone: ${zone}`,
|
|
450
480
|
"; Edit records below, then save and close your editor to apply.",
|
|
451
481
|
"; Format: NAME TTL TYPE CONTENT (use @ for the zone apex)",
|
|
@@ -456,7 +486,7 @@ function renderZoneFile(zone, records) {
|
|
|
456
486
|
const body = rows.map(
|
|
457
487
|
(r) => `${r.name.padEnd(nameW)} ${r.ttl.padEnd(ttlW)} ${r.type.padEnd(typeW)} ${r.content}`
|
|
458
488
|
);
|
|
459
|
-
return [...header, ...body].join("\n")
|
|
489
|
+
return [...header, ...body, "; vim: set ft=bindzone:", ""].join("\n");
|
|
460
490
|
}
|
|
461
491
|
function parseZoneFile(text) {
|
|
462
492
|
const recs = [];
|
|
@@ -464,23 +494,38 @@ function parseZoneFile(text) {
|
|
|
464
494
|
for (let i = 0; i < lines.length; i++) {
|
|
465
495
|
const trimmed = lines[i].trim();
|
|
466
496
|
if (!trimmed || trimmed.startsWith(";")) continue;
|
|
467
|
-
const m = trimmed.match(/^(\S+)\s+(\d+)\s+(?:IN\s+)?([A-Za-z]
|
|
497
|
+
const m = trimmed.match(/^(\S+)\s+(\d+)\s+(?:IN\s+)?([A-Za-z][A-Za-z0-9]*)\s+(.+)$/i);
|
|
468
498
|
if (!m) {
|
|
469
499
|
throw new Error(`line ${i + 1}: could not parse "${trimmed}" (expected: NAME TTL TYPE CONTENT)`);
|
|
470
500
|
}
|
|
471
501
|
const [, name, ttlStr, typeRaw, content] = m;
|
|
472
|
-
|
|
473
|
-
if (type === "SOA") throw new Error(`line ${i + 1}: SOA is managed automatically \u2014 remove it`);
|
|
474
|
-
if (!RECORD_TYPES.includes(type)) throw new Error(`line ${i + 1}: unknown record type "${typeRaw}"`);
|
|
475
|
-
recs.push({ name, type, content: content.trim(), ttl: parseInt(ttlStr) });
|
|
502
|
+
recs.push({ name, type: typeRaw.toUpperCase(), content: content.trim(), ttl: parseInt(ttlStr) });
|
|
476
503
|
}
|
|
477
504
|
return recs;
|
|
478
505
|
}
|
|
479
|
-
function
|
|
480
|
-
|
|
506
|
+
function zoneDiff(before, after) {
|
|
507
|
+
const norm = (t) => t.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith(";")).map((l) => l.replace(/\s+/g, " "));
|
|
508
|
+
const a = new Set(norm(before));
|
|
509
|
+
const b = new Set(norm(after));
|
|
510
|
+
return {
|
|
511
|
+
removed: [...a].filter((l) => !b.has(l)).sort(),
|
|
512
|
+
added: [...b].filter((l) => !a.has(l)).sort()
|
|
513
|
+
};
|
|
481
514
|
}
|
|
482
|
-
function
|
|
483
|
-
|
|
515
|
+
async function prompt(question, valid, def, nonTtyDefault = def) {
|
|
516
|
+
if (!process.stdin.isTTY) return nonTtyDefault;
|
|
517
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
518
|
+
try {
|
|
519
|
+
for (; ; ) {
|
|
520
|
+
const ans = (await rl.question(question)).trim().toLowerCase();
|
|
521
|
+
if (!ans) return def;
|
|
522
|
+
if (valid.includes(ans[0])) return ans[0];
|
|
523
|
+
process.stdout.write(`Please choose one of: ${valid.join(", ")}
|
|
524
|
+
`);
|
|
525
|
+
}
|
|
526
|
+
} finally {
|
|
527
|
+
rl.close();
|
|
528
|
+
}
|
|
484
529
|
}
|
|
485
530
|
function openInEditor(zone, content) {
|
|
486
531
|
const file = path2.join(os3.tmpdir(), `greybull-${zone}-${process.pid}.zone`);
|
|
@@ -498,6 +543,7 @@ function openInEditor(zone, content) {
|
|
|
498
543
|
}
|
|
499
544
|
}
|
|
500
545
|
}
|
|
546
|
+
var ABORTED = "Aborted. No changes made.\n";
|
|
501
547
|
async function editCmd(ctx2, zone) {
|
|
502
548
|
const res = await apiRequest({
|
|
503
549
|
apiUrl: ctx2.apiUrl,
|
|
@@ -505,27 +551,50 @@ async function editCmd(ctx2, zone) {
|
|
|
505
551
|
method: "GET",
|
|
506
552
|
path: `/api/v1/dns/zones/${encodeURIComponent(zone)}/records`
|
|
507
553
|
}).catch((err) => fail(err, ctx2));
|
|
508
|
-
const
|
|
509
|
-
let buffer =
|
|
510
|
-
let banner = "";
|
|
511
|
-
let lastTried = null;
|
|
554
|
+
const originalText = renderZoneFile(zone, res.data);
|
|
555
|
+
let buffer = originalText;
|
|
512
556
|
for (; ; ) {
|
|
513
|
-
const edited = openInEditor(zone,
|
|
557
|
+
const edited = openInEditor(zone, buffer);
|
|
514
558
|
if (edited === null) fail(new Error("Could not launch editor. Set $EDITOR or $VISUAL."), ctx2);
|
|
515
|
-
buffer = stripErrorLines(edited);
|
|
516
|
-
if (normalizeBody(buffer) === normalizeBody(original)) {
|
|
517
|
-
process.stdout.write("No changes.\n");
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
if (lastTried !== null && normalizeBody(buffer) === normalizeBody(lastTried)) {
|
|
521
|
-
fail(new Error("Aborted \u2014 the reported problem was not fixed."), ctx2);
|
|
522
|
-
}
|
|
523
559
|
let records;
|
|
524
560
|
try {
|
|
525
|
-
records = parseZoneFile(
|
|
561
|
+
records = parseZoneFile(edited);
|
|
526
562
|
} catch (err) {
|
|
527
|
-
|
|
528
|
-
|
|
563
|
+
process.stdout.write(`
|
|
564
|
+
Error parsing the zone: ${err.message}
|
|
565
|
+
`);
|
|
566
|
+
const c2 = await prompt(
|
|
567
|
+
"Options are: (e)dit your changes, (r)etry with original zone, (a)bort? [e] ",
|
|
568
|
+
["e", "r", "a"],
|
|
569
|
+
"e",
|
|
570
|
+
"a"
|
|
571
|
+
);
|
|
572
|
+
if (c2 === "a") return void process.stdout.write(ABORTED);
|
|
573
|
+
buffer = c2 === "r" ? originalText : edited;
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const { added, removed } = zoneDiff(originalText, edited);
|
|
577
|
+
if (added.length === 0 && removed.length === 0) {
|
|
578
|
+
return void process.stdout.write("No changes.\n");
|
|
579
|
+
}
|
|
580
|
+
process.stdout.write("\nChanges to apply:\n");
|
|
581
|
+
removed.forEach((l) => process.stdout.write(` - ${l}
|
|
582
|
+
`));
|
|
583
|
+
added.forEach((l) => process.stdout.write(` + ${l}
|
|
584
|
+
`));
|
|
585
|
+
process.stdout.write("\n");
|
|
586
|
+
const c = await prompt(
|
|
587
|
+
"(a)pply these changes, (e)dit again, (r)etry with original zone, (q)uit? [a] ",
|
|
588
|
+
["a", "e", "r", "q"],
|
|
589
|
+
"a"
|
|
590
|
+
);
|
|
591
|
+
if (c === "q") return void process.stdout.write(ABORTED);
|
|
592
|
+
if (c === "e") {
|
|
593
|
+
buffer = edited;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (c === "r") {
|
|
597
|
+
buffer = originalText;
|
|
529
598
|
continue;
|
|
530
599
|
}
|
|
531
600
|
try {
|
|
@@ -536,25 +605,28 @@ async function editCmd(ctx2, zone) {
|
|
|
536
605
|
path: `/api/v1/dns/zones/${encodeURIComponent(zone)}`,
|
|
537
606
|
body: { records }
|
|
538
607
|
});
|
|
539
|
-
process.stdout.write(`Applied ${put.data.applied} record(s)
|
|
608
|
+
process.stdout.write(`Applied. ${zone} now has ${put.data.applied} record(s).
|
|
540
609
|
`);
|
|
541
610
|
return;
|
|
542
611
|
} catch (err) {
|
|
543
612
|
if (err instanceof ApiError && (err.status === 422 || err.status === 400)) {
|
|
544
|
-
|
|
545
|
-
|
|
613
|
+
process.stdout.write(`
|
|
614
|
+
The DNS server rejected the zone: ${err.message}
|
|
615
|
+
`);
|
|
616
|
+
const c2 = await prompt(
|
|
617
|
+
"Options are: (e)dit your changes, (r)etry with original zone, (a)bort? [e] ",
|
|
618
|
+
["e", "r", "a"],
|
|
619
|
+
"e",
|
|
620
|
+
"a"
|
|
621
|
+
);
|
|
622
|
+
if (c2 === "a") return void process.stdout.write(ABORTED);
|
|
623
|
+
buffer = c2 === "r" ? originalText : edited;
|
|
546
624
|
continue;
|
|
547
625
|
}
|
|
548
626
|
fail(err, ctx2);
|
|
549
627
|
}
|
|
550
628
|
}
|
|
551
629
|
}
|
|
552
|
-
function errorBanner(message) {
|
|
553
|
-
return `${ERR_PREFIX}${message}
|
|
554
|
-
${ERR_PREFIX}Fix and save again, or exit without changes to abort.
|
|
555
|
-
;
|
|
556
|
-
`;
|
|
557
|
-
}
|
|
558
630
|
|
|
559
631
|
// src/commands/domains.ts
|
|
560
632
|
async function domainsListCmd(ctx2) {
|
|
@@ -595,7 +667,7 @@ async function domainsCheckCmd(ctx2, query, opts) {
|
|
|
595
667
|
|
|
596
668
|
// src/index.ts
|
|
597
669
|
var program = new Command();
|
|
598
|
-
program.name("greybull").description("Manage Greybull DNS records and domains from the command line").version("0.2.
|
|
670
|
+
program.name("greybull").description("Manage Greybull DNS records and domains from the command line").version("0.2.1").option("--json", "output raw JSON", false).option("--api-url <url>", "portal base URL (or set GREYBULL_API_URL)");
|
|
599
671
|
function ctx(requireAuth = true) {
|
|
600
672
|
const opts = program.opts();
|
|
601
673
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|