Olli Vanhoja
8 years ago
committed by
Leo Lamprecht
7 changed files with 355 additions and 18 deletions
@ -0,0 +1,202 @@ |
|||||
|
#!/usr/bin/env node
|
||||
|
|
||||
|
// Packages
|
||||
|
import chalk from 'chalk' |
||||
|
import minimist from 'minimist' |
||||
|
import ms from 'ms' |
||||
|
import table from 'text-table' |
||||
|
|
||||
|
// Ours
|
||||
|
import * as cfg from '../lib/cfg' |
||||
|
import DomainRecords from '../lib/domain-records' |
||||
|
import indent from '../lib/indent' |
||||
|
import login from '../lib/login' |
||||
|
import strlen from '../lib/strlen' |
||||
|
import {handleError, error} from '../lib/error' |
||||
|
|
||||
|
const argv = minimist(process.argv.slice(2), { |
||||
|
string: ['config'], |
||||
|
boolean: ['help', 'debug'], |
||||
|
alias: { |
||||
|
help: 'h', |
||||
|
config: 'c', |
||||
|
debug: 'd', |
||||
|
token: 't' |
||||
|
} |
||||
|
}) |
||||
|
const subcommand = argv._[0] |
||||
|
|
||||
|
// options
|
||||
|
const help = () => { |
||||
|
console.log(` |
||||
|
${chalk.bold('𝚫 now dns ls')} [domain] |
||||
|
${chalk.bold('𝚫 now dns add')} <domain> <name> <A | AAAA | ALIAS | CNAME | MX> <value> [mx_priority] |
||||
|
${chalk.bold('𝚫 now dns rm')} <id> |
||||
|
|
||||
|
${chalk.dim('Options:')} |
||||
|
|
||||
|
-h, --help output usage information |
||||
|
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline('FILE')} config file |
||||
|
-d, --debug debug mode [off] |
||||
|
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline('TOKEN')} login token |
||||
|
|
||||
|
${chalk.dim('Examples:')} |
||||
|
|
||||
|
${chalk.gray('–')} Listing all your DNS records: |
||||
|
|
||||
|
${chalk.cyan('$ now dns ls')} |
||||
|
|
||||
|
${chalk.gray('–')} Adding an A record for a subdomain: |
||||
|
|
||||
|
${chalk.cyan('$ now dns add zeit.rocks subdomain A 198.51.100.100')} |
||||
|
|
||||
|
${chalk.gray('–')} Adding an MX record (@ as a name refers to the domain): |
||||
|
|
||||
|
${chalk.cyan('$ now dns add zeit.rocks @ MX mail.zeit.rocks 10')} |
||||
|
`)
|
||||
|
} |
||||
|
|
||||
|
// options
|
||||
|
const debug = argv.debug |
||||
|
const apiUrl = argv.url || 'https://api.zeit.co' |
||||
|
if (argv.config) { |
||||
|
cfg.setConfigFile(argv.config) |
||||
|
} |
||||
|
|
||||
|
const exit = code => { |
||||
|
// we give stdout some time to flush out
|
||||
|
// because there's a node bug where
|
||||
|
// stdout writes are asynchronous
|
||||
|
// https://github.com/nodejs/node/issues/6456
|
||||
|
setTimeout(() => process.exit(code || 0), 100) |
||||
|
} |
||||
|
|
||||
|
if (argv.help || !subcommand) { |
||||
|
help() |
||||
|
exit(0) |
||||
|
} else { |
||||
|
const config = cfg.read() |
||||
|
|
||||
|
Promise.resolve(argv.token || config.token || login(apiUrl)) |
||||
|
.then(async token => { |
||||
|
try { |
||||
|
await run(token) |
||||
|
} catch (err) { |
||||
|
handleError(err) |
||||
|
exit(1) |
||||
|
} |
||||
|
}) |
||||
|
.catch(e => { |
||||
|
error(`Authentication error – ${e.message}`) |
||||
|
exit(1) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
async function run(token) { |
||||
|
const domainRecords = new DomainRecords(apiUrl, token, {debug}) |
||||
|
const args = argv._.slice(1) |
||||
|
const start = Date.now() |
||||
|
|
||||
|
if (subcommand === 'ls' || subcommand === 'list') { |
||||
|
if (args.length > 1) { |
||||
|
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now dns ls [domain]`')}`) |
||||
|
return exit(1) |
||||
|
} |
||||
|
|
||||
|
const elapsed = ms(new Date() - start) |
||||
|
const res = await domainRecords.ls(args[0]) |
||||
|
const text = [] |
||||
|
let count = 0 |
||||
|
res.forEach((records, domain) => { |
||||
|
count += records.length |
||||
|
if (records.length > 0) { |
||||
|
const cur = Date.now() |
||||
|
const header = [['', 'id', 'name', 'type', 'value', 'aux', 'created'].map(s => chalk.dim(s))] |
||||
|
const out = table(header.concat(records.map(record => { |
||||
|
const time = chalk.gray(ms(cur - new Date(Number(record.created))) + ' ago') |
||||
|
return [ |
||||
|
'', |
||||
|
record.id, |
||||
|
record.name, |
||||
|
record.type, |
||||
|
record.value, |
||||
|
record.mxPriority ? record.mxPriority : '', |
||||
|
time |
||||
|
] |
||||
|
})), {align: ['l', 'r', 'l', 'l', 'l', 'l'], hsep: ' '.repeat(2), stringLength: strlen}) |
||||
|
text.push(`\n\n${chalk.bold(domain)}\n${indent(out, 2)}`) |
||||
|
} |
||||
|
}) |
||||
|
console.log(`> ${count} record${count === 1 ? '' : 's'} found ${chalk.gray(`[${elapsed}]`)}`) |
||||
|
console.log(text.join('')) |
||||
|
} else if (subcommand === 'add') { |
||||
|
const domain = args[0] |
||||
|
const name = args[1] === '@' ? '' : args[1] |
||||
|
const type = args[2] |
||||
|
const value = args[3] |
||||
|
const mxPriority = args[4] |
||||
|
|
||||
|
if (!(args.length >= 4 && args.length <= 5) || |
||||
|
(type === 'MX' ? !mxPriority : mxPriority)) { |
||||
|
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now dns add <domain> <name> <type> <value> [mx_priority]`')}`) |
||||
|
return exit(1) |
||||
|
} |
||||
|
|
||||
|
const record = await domainRecords.create(domain, {name, type, value, mxPriority}) |
||||
|
const elapsed = ms(new Date() - start) |
||||
|
console.log(`${chalk.cyan('> Success!')} A new DNS record for domain ${chalk.bold(domain)} ${chalk.gray(`(${record.uid})`)} created ${chalk.gray(`[${elapsed}]`)}`) |
||||
|
} else if (subcommand === 'rm' || subcommand === 'remove') { |
||||
|
if (args.length !== 1) { |
||||
|
error(`Invalid number of arguments. Usage: ${chalk.cyan('`now dns rm <id>`')}`) |
||||
|
return exit(1) |
||||
|
} |
||||
|
|
||||
|
const record = await domainRecords.getRecord(args[0]) |
||||
|
if (!record) { |
||||
|
error('DNS record not found') |
||||
|
return exit(1) |
||||
|
} |
||||
|
|
||||
|
const yes = await readConfirmation(record, 'The following record will be removed permanently\n') |
||||
|
if (!yes) { |
||||
|
error('User abort') |
||||
|
return exit(0) |
||||
|
} |
||||
|
|
||||
|
await domainRecords.delete(record.domain, record.id) |
||||
|
const elapsed = ms(new Date() - start) |
||||
|
console.log(`${chalk.cyan('> Success!')} Record ${chalk.gray(`${record.id}`)} removed ${chalk.gray(`[${elapsed}]`)}`) |
||||
|
} else { |
||||
|
error('Please specify a valid subcommand: ls | add | rm') |
||||
|
help() |
||||
|
exit(1) |
||||
|
} |
||||
|
return domainRecords.close() |
||||
|
} |
||||
|
|
||||
|
process.on('uncaughtException', err => { |
||||
|
handleError(err) |
||||
|
exit(1) |
||||
|
}) |
||||
|
|
||||
|
function readConfirmation(record, msg) { |
||||
|
return new Promise(resolve => { |
||||
|
const time = chalk.gray(ms(new Date() - new Date(Number(record.created))) + ' ago') |
||||
|
const tbl = table( |
||||
|
[[record.id, |
||||
|
chalk.bold(`${record.name.length > 0 ? record.name + '.' : ''}${record.domain} ${record.type} ${record.value} ${record.mxPriority ? record.mxPriority : ''}`), |
||||
|
time]], |
||||
|
{align: ['l', 'r', 'l'], hsep: ' '.repeat(6)} |
||||
|
) |
||||
|
|
||||
|
process.stdout.write(`> ${msg}`) |
||||
|
process.stdout.write(' ' + tbl + '\n') |
||||
|
|
||||
|
process.stdout.write(`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[yN] ')}`) |
||||
|
|
||||
|
process.stdin.on('data', d => { |
||||
|
process.stdin.pause() |
||||
|
resolve(d.toString().trim().toLowerCase() === 'y') |
||||
|
}).resume() |
||||
|
}) |
||||
|
} |
@ -0,0 +1,130 @@ |
|||||
|
// Ours
|
||||
|
import Now from '../lib' |
||||
|
|
||||
|
export default class DomainRecords extends Now { |
||||
|
|
||||
|
async getRecord(id) { |
||||
|
const all = (await this.ls()).entries() |
||||
|
for (const [domain, records] of all) { |
||||
|
for (const record of records) { |
||||
|
if (record.id === id) { |
||||
|
record.domain = domain |
||||
|
return record |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return null |
||||
|
} |
||||
|
|
||||
|
async ls(dom) { |
||||
|
let domains |
||||
|
|
||||
|
if (dom) { |
||||
|
domains = [dom] |
||||
|
} else { |
||||
|
const ret = await this.listDomains() |
||||
|
domains = ret.filter(x => !x.isExternal).map(x => x.name).sort((a, b) => a.localeCompare(b)) |
||||
|
} |
||||
|
|
||||
|
const records = new Map() |
||||
|
for (const domain of domains) { |
||||
|
const body = await this.retry(async (bail, attempt) => { |
||||
|
const url = `/domains/${domain}/records` |
||||
|
if (this._debug) { |
||||
|
console.time(`> [debug] #${attempt} GET ${url}`) |
||||
|
} |
||||
|
const res = await this._fetch(url) |
||||
|
if (this._debug) { |
||||
|
console.timeEnd(`> [debug] #${attempt} GET ${url}`) |
||||
|
} |
||||
|
const body = await res.json() |
||||
|
|
||||
|
if (res.status === 404 && body.code === 'not_found') { |
||||
|
return bail(new Error(body.message)) |
||||
|
} else if (res.status !== 200) { |
||||
|
throw new Error(`Failed to get DNS records for domain "${domain}"`) |
||||
|
} |
||||
|
|
||||
|
return body |
||||
|
}) |
||||
|
records.set(domain, body.records.sort((a, b) => a.slug.localeCompare(b.slug))) |
||||
|
} |
||||
|
|
||||
|
return records |
||||
|
} |
||||
|
|
||||
|
create(domain, {name, type, value, mxPriority} = {}) { |
||||
|
const url = `/domains/${domain}/records` |
||||
|
|
||||
|
return this.retry(async (bail, attempt) => { |
||||
|
if (this._debug) { |
||||
|
console.time(`> [debug] #${attempt} POST ${url}`) |
||||
|
} |
||||
|
const res = await this._fetch(url, { |
||||
|
method: 'POST', |
||||
|
body: {name, value, type, mxPriority} |
||||
|
}) |
||||
|
if (this._debug) { |
||||
|
console.timeEnd(`> [debug] #${attempt} POST ${url}`) |
||||
|
} |
||||
|
|
||||
|
const body = await res.json() |
||||
|
if (res.status === 400) { |
||||
|
return bail(new Error(body.error ? body.error.message : 'Unknown error')) |
||||
|
} else if (res.status === 403) { |
||||
|
const err = new Error(`Not authorized to access domain ${name}`) |
||||
|
err.userError = true |
||||
|
return bail(err) |
||||
|
} else if (res.status === 404) { |
||||
|
let err |
||||
|
|
||||
|
if (body.error.code === 'not_found') { |
||||
|
err = new Error(`The domain "${domain}" was not found`) |
||||
|
err.userError = true |
||||
|
return bail(err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (res.status !== 200) { |
||||
|
throw new Error(body.error ? body.error.message : 'Unknown error') |
||||
|
} |
||||
|
|
||||
|
return body |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
delete(domain, recordId) { |
||||
|
const url = `/domains/${domain}/records/${recordId}` |
||||
|
|
||||
|
return this.retry(async (bail, attempt) => { |
||||
|
if (this._debug) { |
||||
|
console.time(`> [debug] #${attempt} DELETE ${url}`) |
||||
|
} |
||||
|
const res = await this._fetch(url, {method: 'DELETE'}) |
||||
|
if (this._debug) { |
||||
|
console.timeEnd(`> [debug] #${attempt} DELETE ${url}`) |
||||
|
} |
||||
|
|
||||
|
const body = await res.json() |
||||
|
if (res.status === 403) { |
||||
|
const err = new Error(`Not authorized to access domain ${domain}`) |
||||
|
err.userError = true |
||||
|
return bail(err) |
||||
|
} else if (res.status === 404) { |
||||
|
let err |
||||
|
|
||||
|
if (body.error.code === 'not_found') { |
||||
|
err = new Error(body.error.message) |
||||
|
err.userError = true |
||||
|
return bail(err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (res.status !== 200) { |
||||
|
throw new Error(body.error ? body.error.message : 'Unkown error') |
||||
|
} |
||||
|
|
||||
|
return body |
||||
|
}) |
||||
|
} |
||||
|
} |
@ -0,0 +1,3 @@ |
|||||
|
export default function indent(text, n) { |
||||
|
return text.split('\n').map(l => ' '.repeat(n) + l).join('\n') |
||||
|
} |
Loading…
Reference in new issue