digital-ocean-dynamic-dns
Advanced tools
Comparing version 1.0.1 to 1.1.2
21
index.js
@@ -0,3 +1,22 @@ | ||
const buildDomains = require('./lib/build-domains') | ||
const updateDomain = require('./lib/update-domain') | ||
module.exports = opts => updateDomain(opts).catch(e => process.exit(1)) | ||
module.exports = async args => { | ||
let domains | ||
try { | ||
domains = await buildDomains(args) | ||
} catch (e) { | ||
process.exit(1) | ||
} | ||
const updater = updateDomain() | ||
for (const domain of domains) { | ||
try { | ||
await updater(domain) | ||
} catch (e) { | ||
process.exit(1) | ||
} | ||
} | ||
process.exit(0) | ||
} |
@@ -1,23 +0,13 @@ | ||
const request = require('request-promise') | ||
const fetch = require('node-fetch') | ||
module.exports.listDomainRecords = ({ token, domainName }) => | ||
request({ | ||
method: 'GET', | ||
url: `https://api.digitalocean.com/v2/domains/${domainName}/records`, | ||
json: true, | ||
headers: { | ||
'content-type': 'application/json', | ||
authorization: `Bearer ${token}` | ||
} | ||
jsonFetch(`https://api.digitalocean.com/v2/domains/${domainName}/records`, { | ||
token, | ||
method: 'GET' | ||
}).then(res => res.domain_records) | ||
module.exports.createDomainRecord = ({ token, domainName, domainRecord }) => | ||
request({ | ||
jsonFetch(`https://api.digitalocean.com/v2/domains/${domainName}/records`, { | ||
token, | ||
method: 'POST', | ||
url: `https://api.digitalocean.com/v2/domains/${domainName}/records`, | ||
json: true, | ||
headers: { | ||
'content-type': 'application/json', | ||
authorization: `Bearer ${token}` | ||
}, | ||
body: domainRecord | ||
@@ -32,11 +22,36 @@ }).then(res => res.domain_record) | ||
}) => | ||
request({ | ||
method: 'PUT', | ||
url: `https://api.digitalocean.com/v2/domains/${domainName}/records/${domainRecordId}`, | ||
json: true, | ||
jsonFetch( | ||
`https://api.digitalocean.com/v2/domains/${domainName}/records/${domainRecordId}`, | ||
{ | ||
token, | ||
method: 'PUT', | ||
body: updateData | ||
} | ||
).then(res => res.domain_record) | ||
async function jsonFetch (url, { method, token, body }) { | ||
const fetchOpts = { | ||
method, | ||
headers: { | ||
'content-type': 'application/json', | ||
authorization: `Bearer ${token}` | ||
}, | ||
body: updateData | ||
}).then(res => res.domain_record) | ||
} | ||
} | ||
if (body) { | ||
fetchOpts.body = JSON.stringify(body) | ||
} | ||
const res = await fetch(url, fetchOpts) | ||
if (!res.ok) { | ||
const result = await res.text() | ||
throw new Error( | ||
'DigitalOcean API request failed with status ' + | ||
res.status + | ||
': ' + | ||
result | ||
) | ||
} | ||
return res.json() | ||
} |
@@ -1,3 +0,3 @@ | ||
const mockRequest = jest.fn(() => Promise.resolve()) | ||
jest.setMock('request-promise', mockRequest) | ||
jest.mock('node-fetch') | ||
const mockFetch = require('node-fetch') | ||
@@ -7,4 +7,13 @@ const api = require('./do-api') | ||
describe('lib/do-api', () => { | ||
afterEach(() => jest.clearAllMocks()) | ||
afterEach(() => jest.resetAllMocks()) | ||
function createResponse (status, data) { | ||
return { | ||
status, | ||
ok: status >= 200 && status < 300, | ||
text: () => Promise.resolve(JSON.stringify(data)), | ||
json: () => Promise.resolve(data) | ||
} | ||
} | ||
describe('listDomainRecords()', () => { | ||
@@ -15,10 +24,11 @@ it('fetches a list of domain records', () => { | ||
const domainRecords = [{ record: 1 }, { record: 2 }] | ||
mockRequest.mockReturnValueOnce( | ||
Promise.resolve({ domain_records: domainRecords }) | ||
mockFetch.mockReturnValue( | ||
Promise.resolve(createResponse(200, { domain_records: domainRecords })) | ||
) | ||
return api.listDomainRecords({ token, domainName }).then(result => { | ||
const reqOpts = mockRequest.mock.calls[0][0] | ||
const reqUrl = mockFetch.mock.calls[0][0] | ||
const reqOpts = mockFetch.mock.calls[0][1] | ||
expect(reqUrl).toContain(domainName) | ||
expect(reqOpts.method).toBe('GET') | ||
expect(reqOpts.url).toContain(domainName) | ||
expect(reqOpts.headers['content-type']).toBe('application/json') | ||
@@ -29,2 +39,15 @@ expect(reqOpts.headers.authorization).toBe(`Bearer ${token}`) | ||
}) | ||
it('rejects if request fails', async () => { | ||
const token = 'abc123' | ||
const domainName = 'domain.test' | ||
const domainRecords = [{ record: 1 }, { record: 2 }] | ||
mockFetch.mockReturnValue( | ||
Promise.resolve(createResponse(501, { some: 'error' })) | ||
) | ||
const result = await await expect( | ||
api.listDomainRecords({ token, domainName }) | ||
).rejects.toThrow(/501/) | ||
}) | ||
}) | ||
@@ -38,4 +61,4 @@ | ||
const domainRecord = { updatedRecord: 1 } | ||
mockRequest.mockReturnValueOnce( | ||
Promise.resolve({ domain_record: domainRecord }) | ||
mockFetch.mockReturnValue( | ||
Promise.resolve(createResponse(200, { domain_record: domainRecord })) | ||
) | ||
@@ -46,8 +69,9 @@ | ||
.then(result => { | ||
const reqOpts = mockRequest.mock.calls[0][0] | ||
const reqUrl = mockFetch.mock.calls[0][0] | ||
const reqOpts = mockFetch.mock.calls[0][1] | ||
expect(reqUrl).toContain(domainName) | ||
expect(reqOpts.method).toBe('POST') | ||
expect(reqOpts.url).toContain(domainName) | ||
expect(reqOpts.headers['content-type']).toBe('application/json') | ||
expect(reqOpts.headers.authorization).toBe(`Bearer ${token}`) | ||
expect(reqOpts.body).toEqual(domainRecord) | ||
expect(reqOpts.body).toEqual(JSON.stringify(domainRecord)) | ||
expect(result).toEqual(domainRecord) | ||
@@ -65,4 +89,4 @@ }) | ||
const domainRecord = { updatedRecord: 1 } | ||
mockRequest.mockReturnValueOnce( | ||
Promise.resolve({ domain_record: domainRecord }) | ||
mockFetch.mockReturnValue( | ||
Promise.resolve(createResponse(200, { domain_record: domainRecord })) | ||
) | ||
@@ -73,9 +97,10 @@ | ||
.then(result => { | ||
const reqOpts = mockRequest.mock.calls[0][0] | ||
const reqUrl = mockFetch.mock.calls[0][0] | ||
const reqOpts = mockFetch.mock.calls[0][1] | ||
expect(reqUrl).toContain(domainName) | ||
expect(reqUrl).toContain(domainRecordId) | ||
expect(reqOpts.method).toBe('PUT') | ||
expect(reqOpts.url).toContain(domainName) | ||
expect(reqOpts.url).toContain(domainRecordId) | ||
expect(reqOpts.headers['content-type']).toBe('application/json') | ||
expect(reqOpts.headers.authorization).toBe(`Bearer ${token}`) | ||
expect(reqOpts.body).toEqual(updateData) | ||
expect(reqOpts.body).toEqual(JSON.stringify(updateData)) | ||
expect(result).toEqual(domainRecord) | ||
@@ -82,0 +107,0 @@ }) |
const publicIp = require('public-ip') | ||
const R = require('ramda') | ||
const check = require('check') | ||
const co = require('co') | ||
@@ -9,42 +7,15 @@ const api = require('./do-api') | ||
module.exports = function (opts) { | ||
return validateOpts(opts) | ||
.then(ensureAddress) | ||
.then(updateOrCreateDomainRecord) | ||
.catch(e => { | ||
log.error(e) | ||
throw e | ||
}) | ||
} | ||
module.exports = function build () { | ||
let pendingIp | ||
function validateOpts (opts = {}) { | ||
const completeOpts = { | ||
token: opts.token, | ||
address: opts.address, | ||
domainName: opts.domainName, | ||
recordName: opts.recordName, | ||
recordType: R.propOr('A', 'recordType', opts), | ||
recordTtl: R.propOr(null, 'recordTtl', opts), | ||
recordPriority: R.propOr(null, 'recordPriority', opts), | ||
recordPort: R.propOr(null, 'recordPort', opts), | ||
recordWeight: R.propOr(null, 'recordWeight', opts), | ||
create: R.propOr(false, 'create', opts) | ||
return domain => { | ||
return ensureAddress(domain) | ||
.then(updateOrCreateDomainRecord) | ||
.catch(e => { | ||
log.error(e) | ||
throw e | ||
}) | ||
} | ||
const errors = check(completeOpts) | ||
.hasString('token') | ||
.hasString('domainName') | ||
.hasString('recordName') | ||
.errors() | ||
if (errors.length) { | ||
errors.forEach(log.error) | ||
return Promise.reject('Missing or invalid arguments.') | ||
} | ||
return Promise.resolve(completeOpts) | ||
} | ||
function ensureAddress (opts) { | ||
return co(function * () { | ||
async function ensureAddress (opts) { | ||
if (opts.address) { | ||
@@ -55,13 +26,15 @@ log.info(`Using address: ${opts.address}`) | ||
log.info('Obtain public IP address') | ||
const ip = yield publicIp.v4() | ||
log.info(`Public IP: ${ip}`) | ||
if (!pendingIp) { | ||
log.info('Obtaining public IP address') | ||
pendingIp = publicIp.v4() | ||
} | ||
const ip = await pendingIp | ||
log.info(`Using public IP address: ${ip}`) | ||
return Object.assign({}, opts, { address: ip }) | ||
}) | ||
} | ||
} | ||
function updateOrCreateDomainRecord (opts) { | ||
return co(function * () { | ||
const domainRecord = yield getExistingDomainRecord(opts) | ||
async function updateOrCreateDomainRecord (opts) { | ||
const domainRecord = await getExistingDomainRecord(opts) | ||
@@ -73,13 +46,12 @@ if (!domainRecord && opts.create) { | ||
} else { | ||
throw new Error( | ||
log.error( | ||
`Cannot find domain record "${opts.recordName}.${opts.domainName}". Set the --create flag if you want to create it.` | ||
) | ||
throw new Error('Domain record not found') | ||
} | ||
}) | ||
} | ||
} | ||
function getExistingDomainRecord (opts) { | ||
return co(function * () { | ||
async function getExistingDomainRecord (opts) { | ||
log.info('Fetch existing Domain records') | ||
const domainRecords = yield api.listDomainRecords({ | ||
const domainRecords = await api.listDomainRecords({ | ||
token: opts.token, | ||
@@ -89,7 +61,5 @@ domainName: opts.domainName | ||
return domainRecords.find(r => r.name === opts.recordName) || false | ||
}) | ||
} | ||
} | ||
function createDomainRecord (opts) { | ||
return co(function * () { | ||
async function createDomainRecord (opts) { | ||
const domainRecord = { | ||
@@ -109,3 +79,3 @@ type: opts.recordType, | ||
const newDomainRecord = yield api.createDomainRecord({ | ||
const newDomainRecord = await api.createDomainRecord({ | ||
token: opts.token, | ||
@@ -119,7 +89,7 @@ domainName: opts.domainName, | ||
) | ||
}) | ||
} | ||
function updateDomainRecord (opts, domainRecord) { | ||
co(function * () { | ||
return opts | ||
} | ||
async function updateDomainRecord (opts, domainRecord) { | ||
if (opts.address === domainRecord.data) { | ||
@@ -146,3 +116,3 @@ log.info( | ||
const updatedRecord = yield api.updateDomainRecord({ | ||
const updatedRecord = await api.updateDomainRecord({ | ||
token: opts.token, | ||
@@ -155,3 +125,5 @@ domainName: opts.domainName, | ||
log.info(`Success updating domain record: ${JSON.stringify(updatedRecord)}`) | ||
}) | ||
return opts | ||
} | ||
} |
@@ -23,12 +23,4 @@ const mockPublicIp = { | ||
describe('lib/update-domain', () => { | ||
afterEach(() => jest.clearAllMocks()) | ||
afterEach(() => jest.resetAllMocks()) | ||
it('rejects if no opts are provided', () => { | ||
return expect(updateDomain()).rejects.toBeDefined() | ||
}) | ||
it('rejects if invalid opts are provided', () => { | ||
return expect(updateDomain({ invalid: 'opts' })).rejects.toBeDefined() | ||
}) | ||
it('uses a provided address', () => { | ||
@@ -51,3 +43,3 @@ const ip = '123.456.789.123' | ||
return updateDomain(opts).then(() => { | ||
return updateDomain()(opts).then(() => { | ||
const updateOpts = mockApi.updateDomainRecord.mock.calls[0][0] | ||
@@ -75,3 +67,3 @@ expect(updateOpts.updateData.data).toEqual(ip) | ||
return updateDomain(opts).then(() => { | ||
return updateDomain()(opts).then(() => { | ||
expect(mockPublicIp.v4).toHaveBeenCalledWith() | ||
@@ -83,2 +75,39 @@ const updateOpts = mockApi.updateDomainRecord.mock.calls[0][0] | ||
it('updates multiple domains with same public IP', async () => { | ||
const domain1 = { | ||
token: '123abc', | ||
domainName: 'domain.test', | ||
recordName: 'test1' | ||
} | ||
const domain2 = { | ||
token: '123abc', | ||
domainName: 'domain.test', | ||
recordName: 'test2' | ||
} | ||
const ip = '123.456.789.123' | ||
mockPublicIp.v4.mockReturnValue(Promise.resolve(ip)) | ||
mockApi.listDomainRecords.mockReturnValue( | ||
Promise.resolve([ | ||
{ | ||
name: domain1.recordName | ||
}, | ||
{ | ||
name: domain2.recordName | ||
} | ||
]) | ||
) | ||
const updater = updateDomain() | ||
await updater(domain1) | ||
await updater(domain2) | ||
const domain1Opts = mockApi.updateDomainRecord.mock.calls[0][0] | ||
expect(domain1Opts.updateData.data).toEqual(ip) | ||
const domain2Opts = mockApi.updateDomainRecord.mock.calls[0][0] | ||
expect(domain2Opts.updateData.data).toEqual(ip) | ||
}) | ||
it('updates an existing domain record with minimum opts', () => { | ||
@@ -102,3 +131,3 @@ const ip = '123.456.789.123' | ||
return updateDomain(opts).then(() => { | ||
return updateDomain()(opts).then(() => { | ||
const updateOpts = mockApi.updateDomainRecord.mock.calls[0][0] | ||
@@ -108,4 +137,3 @@ expect(updateOpts.token).toEqual(opts.token) | ||
expect(updateOpts.domainRecordId).toEqual(domainRecord.id) | ||
expect(updateOpts.updateData).toEqual({ | ||
type: 'A', | ||
expect(updateOpts.updateData).toMatchObject({ | ||
name: opts.recordName, | ||
@@ -140,3 +168,3 @@ data: ip | ||
return updateDomain(opts).then(() => { | ||
return updateDomain()(opts).then(() => { | ||
const updateOpts = mockApi.updateDomainRecord.mock.calls[0][0] | ||
@@ -177,3 +205,3 @@ expect(updateOpts.token).toEqual(opts.token) | ||
return updateDomain(opts).then(() => { | ||
return updateDomain()(opts).then(() => { | ||
expect(mockApi.updateDomainRecord).toHaveBeenCalledTimes(0) | ||
@@ -194,3 +222,3 @@ expect(mockApi.createDomainRecord).toHaveBeenCalledTimes(0) | ||
return expect(updateDomain(opts)).rejects.toBeDefined() | ||
return expect(updateDomain()(opts)).rejects.toBeDefined() | ||
}) | ||
@@ -221,3 +249,3 @@ | ||
return updateDomain(opts).then(() => { | ||
return updateDomain()(opts).then(() => { | ||
const createOpts = mockApi.createDomainRecord.mock.calls[0][0] | ||
@@ -237,29 +265,2 @@ expect(createOpts.token).toEqual(opts.token) | ||
}) | ||
it('creates a new domain record with default values', () => { | ||
const opts = { | ||
token: '123abc', | ||
address: '123.456.789.123', | ||
recordName: 'test', | ||
domainName: 'domain.test', | ||
create: true | ||
} | ||
mockApi.listDomainRecords.mockReturnValueOnce(Promise.resolve([])) | ||
return updateDomain(opts).then(() => { | ||
const createOpts = mockApi.createDomainRecord.mock.calls[0][0] | ||
expect(createOpts.token).toEqual(opts.token) | ||
expect(createOpts.domainName).toEqual(opts.domainName) | ||
expect(createOpts.domainRecord).toEqual({ | ||
type: 'A', | ||
name: opts.recordName, | ||
data: opts.address, | ||
priority: null, | ||
port: null, | ||
ttl: null, | ||
weight: null | ||
}) | ||
}) | ||
}) | ||
}) |
{ | ||
"name": "digital-ocean-dynamic-dns", | ||
"version": "1.0.1", | ||
"version": "1.1.2", | ||
"description": "Dynamically update Domain Records on Digital Ocean", | ||
@@ -29,3 +29,3 @@ "author": "Philip Klostermann <philip.klostermann@gmail.com>", | ||
"test:watch": "jest --watch", | ||
"prepublish": "npm run test" | ||
"prepublishOnly": "npm run test" | ||
}, | ||
@@ -35,12 +35,10 @@ "dependencies": { | ||
"check": "^1.0.0", | ||
"co": "^4.6.0", | ||
"commander": "^2.9.0", | ||
"fs-extra": "^9.0.1", | ||
"node-fetch": "^2.6.1", | ||
"public-ip": "^2.3.5", | ||
"ramda": "^0.24.1", | ||
"request": "^2.81.0", | ||
"request-promise": "^4.2.1" | ||
"ramda": "^0.24.1" | ||
}, | ||
"devDependencies": { | ||
"@types/jest": "^19.2.4", | ||
"jest": "^20.0.4" | ||
"jest": "^26.6.3" | ||
}, | ||
@@ -47,0 +45,0 @@ "jest": { |
# digital-ocean-dynamic-dns | ||
Dynamically update Domain Records on Digital Ocean. | ||
@@ -48,2 +49,26 @@ | ||
### Config file | ||
Using the `-f` or `--file` option, all parameters can be set from a JSON file. It's possible to specify multiple domains when the JSON file contains an array of domains. Any arguments passed on the command line will also be applied but will be overridden by the file contents. | ||
``` | ||
$ dodd -t abc123def456ghi789 -f /path/to/file.json | ||
``` | ||
`/path/to/file.json` | ||
``` | ||
[ | ||
{ | ||
"domainName": "domain.tld", | ||
"recordName": "record" | ||
}, | ||
{ | ||
"token": "xyz987uvw654qrs321", | ||
"domainName": "other.tld", | ||
"recordName": "test" | ||
} | ||
] | ||
``` | ||
## Run automatically | ||
@@ -93,2 +118,1 @@ | ||
``` | ||
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
7
1
17
117
28184
635
2
2
+ Addedfs-extra@^9.0.1
+ Addednode-fetch@^2.6.1
+ Addedat-least-node@1.0.0(transitive)
+ Addedfs-extra@9.1.0(transitive)
+ Addedgraceful-fs@4.2.11(transitive)
+ Addedjsonfile@6.1.0(transitive)
+ Addednode-fetch@2.7.0(transitive)
+ Addedtr46@0.0.3(transitive)
+ Addeduniversalify@2.0.1(transitive)
+ Addedwebidl-conversions@3.0.1(transitive)
+ Addedwhatwg-url@5.0.0(transitive)
- Removedco@^4.6.0
- Removedrequest@^2.81.0
- Removedrequest-promise@^4.2.1
- Removedajv@6.12.6(transitive)
- Removedasn1@0.2.6(transitive)
- Removedassert-plus@1.0.0(transitive)
- Removedasynckit@0.4.0(transitive)
- Removedaws-sign2@0.7.0(transitive)
- Removedaws4@1.13.2(transitive)
- Removedbcrypt-pbkdf@1.0.2(transitive)
- Removedbluebird@3.7.2(transitive)
- Removedcaseless@0.12.0(transitive)
- Removedco@4.6.0(transitive)
- Removedcombined-stream@1.0.8(transitive)
- Removedcore-util-is@1.0.2(transitive)
- Removeddashdash@1.14.1(transitive)
- Removeddelayed-stream@1.0.0(transitive)
- Removedecc-jsbn@0.1.2(transitive)
- Removedextend@3.0.2(transitive)
- Removedextsprintf@1.3.0(transitive)
- Removedfast-deep-equal@3.1.3(transitive)
- Removedfast-json-stable-stringify@2.1.0(transitive)
- Removedforever-agent@0.6.1(transitive)
- Removedform-data@2.3.3(transitive)
- Removedgetpass@0.1.7(transitive)
- Removedhar-schema@2.0.0(transitive)
- Removedhar-validator@5.1.5(transitive)
- Removedhttp-signature@1.2.0(transitive)
- Removedis-typedarray@1.0.0(transitive)
- Removedisstream@0.1.2(transitive)
- Removedjsbn@0.1.1(transitive)
- Removedjson-schema@0.4.0(transitive)
- Removedjson-schema-traverse@0.4.1(transitive)
- Removedjson-stringify-safe@5.0.1(transitive)
- Removedjsprim@1.4.2(transitive)
- Removedlodash@4.17.21(transitive)
- Removedmime-db@1.52.0(transitive)
- Removedmime-types@2.1.35(transitive)
- Removedoauth-sign@0.9.0(transitive)
- Removedperformance-now@2.1.0(transitive)
- Removedpsl@1.13.0(transitive)
- Removedpunycode@2.3.1(transitive)
- Removedqs@6.5.3(transitive)
- Removedrequest@2.88.2(transitive)
- Removedrequest-promise@4.2.6(transitive)
- Removedrequest-promise-core@1.1.4(transitive)
- Removedsafer-buffer@2.1.2(transitive)
- Removedsshpk@1.18.0(transitive)
- Removedstealthy-require@1.1.1(transitive)
- Removedtough-cookie@2.5.0(transitive)
- Removedtunnel-agent@0.6.0(transitive)
- Removedtweetnacl@0.14.5(transitive)
- Removeduri-js@4.4.1(transitive)
- Removeduuid@3.4.0(transitive)
- Removedverror@1.10.0(transitive)