Adds simple diffing for records + updateRecords

This commit is contained in:
Akshay Nair committed 2020-10-11 21:56:29 +05:30
1 parent fea85040ce
commit 281f88ea5f
3 files changed
+124 -110

No files matched your search

+57 -47
View File
@@ -1,14 +1,22 @@
const R = require('ramda'); const R = require('ramda');
const { getDomainService, diffRecords } = require('../utils/domain-service'); const { getDomainService, diffRecords } = require('../utils/domain-service');
const {DOMAIN_DOMAIN} = require('../utils/constants');
const getCpanel = ({ zone, redir, setZone, setRedir } = {}) => ({ const getCpanel = ({ zone, addZone, editZone, redir, addRedir, editRedir } = {}) => ({
addZoneRecord: (rec) => setZone(rec), zone: {
addRedirection: (rec) => setRedir(rec), fetch: (_) => zone(),
fetchZoneRecords: (_) => zone(), add: (rec) => addZone(rec),
fetchRedirections: (_) => redir(), edit: (rec) => editZone(rec),
},
redirection: {
fetch: (_) => redir(),
add: (rec) => addRedir(rec),
edit: (rec) => editZone(rec),
},
}); });
describe('diffRecords', () => { describe('diffRecords', () => {
return;
it('should show added record', () => { it('should show added record', () => {
const oldRecords = [ const oldRecords = [
{ name: 'xx', type: 'CNAME', address: 'fck.com.' }, { name: 'xx', type: 'CNAME', address: 'fck.com.' },
@@ -70,21 +78,28 @@ describe('diffRecords', () => {
}); });
describe('Domain service', () => { describe('Domain service', () => {
const setZone = jest.fn(async () => ({})); const addZone = jest.fn(async () => ({}));
const setRedir = jest.fn(async () => ({})); const editZone = jest.fn(async () => ({}));
const addRedir = jest.fn(async () => ({}));
const editRedir = jest.fn(async () => ({}));
const mockDS = ({ zones, redirections }) => getDomainService({ cpanel: getCpanel({ const mockDS = ({ zones, redirections }) => getDomainService({ cpanel: getCpanel({
zone: async () => ({ hosts: zones }), zone: async () => zones,
redir: async () => ({ hosts: redirections }), redir: async () => redirections,
setZone, addZone,
setRedir, addRedir,
editZone,
editRedir,
}) }); }) });
const getZoneCalls = () => setZone.mock.calls.map(R.head).map(R.pick(['name', 'type', 'address'])); const getRecordCalls = recfn => recfn.mock.calls.map(R.head).map(R.pick(['name', 'type', 'address']));
const getZoneCalls = () => getRecordCalls(addZone);
beforeEach(() => { beforeEach(() => {
setZone.mockClear(); addZone.mockClear();
setRedir.mockClear(); editZone.mockClear();
addRedir.mockClear();
editRedir.mockClear();
}); });
describe('getHosts', () => { describe('getHosts', () => {
@@ -137,19 +152,19 @@ describe('Domain service', () => {
{ name: 'foo1', type: 'URL', address: 'https://duck.com' }, { name: 'foo1', type: 'URL', address: 'https://duck.com' },
]; ];
const setZone = jest.fn(async () => {}); const addZone = jest.fn(async () => {});
const setRedir = jest.fn(async () => {}); const addRedir = jest.fn(async () => {});
const mockDomainService = getDomainService({ cpanel: getCpanel({ setZone, setRedir }) }); const mockDomainService = getDomainService({ cpanel: getCpanel({ addZone, addRedir }) });
await mockDomainService.setHosts(records); await mockDomainService.setHosts(records);
expect(setZone).toBeCalledTimes(2); expect(addZone).toBeCalledTimes(2);
expect(setRedir).toBeCalledTimes(2); expect(addRedir).toBeCalledTimes(2);
expect(setZone.mock.calls.map(R.head)).toEqual([ expect(addZone.mock.calls.map(R.head)).toEqual([
{ name: 'xx', type: 'CNAME', address: 'fck.com' }, { name: 'xx', type: 'CNAME', address: 'fck.com' },
{ name: 'xx', type: 'A', address: '111.1.1212.1' }, { name: 'xx', type: 'A', address: '111.1.1212.1' },
]); ]);
expect(setRedir.mock.calls.map(R.head)).toEqual([ expect(addRedir.mock.calls.map(R.head)).toEqual([
{ {
domain: 'foo.booboo.xyz', domain: 'foo.booboo.xyz',
redirect: 'https://google.com', redirect: 'https://google.com',
@@ -169,70 +184,65 @@ describe('Domain service', () => {
}); });
describe('updateHosts', () => { describe('updateHosts', () => {
return;
it('should append new hosts with existing ones and set it', async () => { it('should append new hosts with existing ones and set it', async () => {
const zones = [ const zones = [
{ HostId: 1, name: 'a', type: 'CNAME', address: 'boo' }, { someid: 1, name: 'a', type: 'CNAME', address: 'boo' },
{ HostId: 2, name: 'b', type: 'CNAME', address: 'goo' }, { someid: 2, name: 'b', type: 'CNAME', address: 'goo' },
]; ];
const redirections = []; const redirections = [];
const mockDomainService = mockDS({ zones, redirections });; const mockDomainService = mockDS({ zones, redirections });
await mockDomainService.updateHosts([ await mockDomainService.updateHosts([
{ name: 'a', type: 'CNAME', address: 'boo' }, { name: 'a', type: 'CNAME', address: 'boo' },
{ name: 'b', type: 'CNAME', address: 'goo' }, { name: 'b', type: 'CNAME', address: 'goo' },
{ name: 'c', type: 'A', address: '12.131321.213' }, { name: 'c', type: 'A', address: '12.131321.213' },
]); ]);
expect(setZone).toBeCalledTimes(1); expect(addZone).toBeCalledTimes(1);
expect(getZoneCalls()).toEqual([ expect(getRecordCalls(addZone)).toEqual([
{ name: 'c', type: 'A', address: '12.131321.213' }, { name: 'c', type: 'A', address: '12.131321.213' },
]); ]);
expect(editZone).toBeCalledTimes(0);
}); });
it('should update matching host and set it', async () => { it('should update matching host and set it', async () => {
const zones = [ const zones = [
{ HostId: 1, Name: 'a', Type: 'CNAME', address: 'boo' }, { someid: 1, name: 'a', type: 'CNAME', address: 'boo' },
{ HostId: 2, Name: 'b', Type: 'CNAME', address: 'goo' }, { someid: 2, name: 'b', type: 'CNAME', address: 'goo' },
]; ];
const redirections = []; const redirections = [];
const mockDomainService = mockDS({ zones, redirections });; const mockDomainService = mockDS({ zones, redirections });
await mockDomainService.updateHosts([ await mockDomainService.updateHosts([
{ name: 'a', type: 'CNAME', address: 'boo' }, { name: 'a', type: 'CNAME', address: 'boo' },
{ name: 'b', type: 'CNAME', address: 'googoogaga' }, { name: 'b', type: 'CNAME', address: 'googoogaga' },
]); ]);
expect(setZone).toBeCalledTimes(2); expect(addZone).toBeCalledTimes(0);
expect(getZoneCalls()).toEqual([ expect(editZone).toBeCalledTimes(1);
{ name: 'a', type: 'CNAME', address: 'boo' }, expect(getRecordCalls(editZone)).toEqual([
{ name: 'b', type: 'CNAME', address: 'googoogaga' }, { name: 'b', type: 'CNAME', address: 'googoogaga' },
]); ]);
}); });
return;
it('should update matching host and set it', async () => { it('should update matching host and set it', async () => {
const records = [ const zones = [
{ HostId: 1, Name: 'a', Type: 'CNAME', address: 'boo' }, { someid: 1, name: 'a', type: 'CNAME', address: 'boo' },
{ HostId: 2, Name: 'b', Type: 'CNAME', address: 'goo' }, { someid: 2, name: 'b', type: 'CNAME', address: 'goo' },
{ HostId: 2, Name: 'b', Type: 'CNAME', address: 'xaa' }, { someid: 2, name: 'b', type: 'CNAME', address: 'xaa' },
]; ];
const redirections = [];
const onGet = () => Promise.resolve({ hosts: records }); const mockDomainService = mockDS({ zones, redirections });
const onSet = jest.fn(async () => ({}));
const mockDomainService = getDomainService({ cpanel: getCpanel({ onSet, onGet }) });
await mockDomainService.updateHosts([ await mockDomainService.updateHosts([
{ name: 'a', type: 'CNAME', address: 'boo' }, { name: 'a', type: 'CNAME', address: 'boo' },
{ name: 'b', type: 'CNAME', address: 'googoogaga' }, { name: 'b', type: 'CNAME', address: 'googoogaga' },
{ name: 'b', type: 'CNAME', address: 'farboo' }, { name: 'b', type: 'CNAME', address: 'farboo' },
]); ]);
const [hosts] = onSet.mock.calls[0]; expect(addZone).toBeCalledTimes(0);
expect(editZone).toBeCalledTimes(2);
expect(hosts.map(R.pick(['name', 'type', 'address']))).toEqual([ expect(getRecordCalls(editZone)).toEqual([
{ name: 'a', type: 'CNAME', address: 'boo' },
{ name: 'b', type: 'CNAME', address: 'googoogaga' }, { name: 'b', type: 'CNAME', address: 'googoogaga' },
{ name: 'b', type: 'CNAME', address: 'farboo' }, { name: 'b', type: 'CNAME', address: 'farboo' },
]); ]);
+41 -45
View File
@@ -1,6 +1,6 @@
const R = require('ramda'); const R = require('ramda');
const { cpanel } = require('./lib/cpanel'); const { cpanel } = require('./lib/cpanel');
const {DOMAIN_DOMAIN} = require('./constants'); const { DOMAIN_DOMAIN } = require('./constants');
const flattenPromise = xs => Promise.all(xs); const flattenPromise = xs => Promise.all(xs);
@@ -32,50 +32,6 @@ const toHostMap = hosts => hosts.reduce((acc, host) => {
return { ...acc, [key]: [ ...(acc[key] || []), host ] }; return { ...acc, [key]: [ ...(acc[key] || []), host ] };
}, {}); }, {});
const getDomainService = ({ cpanel }) => {
let hostList = [];
const fetchZoneRecords = () => cpanel.fetchZoneRecords().then(R.map(zoneToRecord));
const fetchRedirections = () => cpanel.fetchRedirections().then(R.map(redirectionToRecord));
const addZoneRecord = R.compose(cpanel.addZoneRecord, recordToZone);
const addRedirection = R.compose(cpanel.addRedirection, recordToRedirection);
const getHosts = async () => {
if (hostList.length) return hostList;
const list = await Promise.all([fetchZoneRecords(), fetchRedirections()]).then(R.flatten);
hostList = list;
return list;
};
const setHosts = R.compose(flattenPromise, R.map(R.cond([
[ R.propEq('type', 'URL'), addRedirection ],
[ R.T, addZoneRecord ],
])));
const updateHosts = async hosts => {
const hostList = await getHosts();
const remoteHostMap = toHostMap(hostList);
const localHostMap = toHostMap(hosts);
const newHostList = R.toPairs(localHostMap).reduce((acc, [key, local]) => {
const remote = remoteHostMap[key];
if (remote) {
return acc.concat(local.map((localItem, index) => R.merge(remote[index], localItem)));
}
return [...acc, ...local];
}, []);
return setHosts(newHostList);
};
return { getHosts, setHosts, updateHosts };
};
const diffRecords = (oldRecords, newRecords) => { const diffRecords = (oldRecords, newRecords) => {
const remoteHostMap = toHostMap(oldRecords); const remoteHostMap = toHostMap(oldRecords);
const localHostMap = toHostMap(newRecords); const localHostMap = toHostMap(newRecords);
@@ -102,6 +58,46 @@ const diffRecords = (oldRecords, newRecords) => {
}, { add: [], edit: [] }); }, { add: [], edit: [] });
}; };
const getDomainService = ({ cpanel }) => {
let hostList = [];
const fetchZoneRecords = () => cpanel.zone.fetch().then(R.map(zoneToRecord));
const fetchRedirections = () => cpanel.redirection.fetch().then(R.map(redirectionToRecord));
const addZoneRecord = R.compose(cpanel.zone.add, recordToZone);
const addRedirection = R.compose(cpanel.redirection.add, recordToRedirection);
const editZoneRecord = R.compose(cpanel.zone.edit, recordToZone);
const editRedirection = R.compose(cpanel.redirection.edit, recordToRedirection);
const getHosts = async () => {
if (hostList.length) return hostList;
const list = await Promise.all([fetchZoneRecords(), fetchRedirections()]).then(R.flatten);
hostList = list;
return list;
};
const addRecords = R.compose(flattenPromise, R.map(R.cond([
[ R.propEq('type', 'URL'), addRedirection ],
[ R.T, addZoneRecord ],
])));
const editRecords = R.compose(flattenPromise, R.map(R.cond([
[ R.propEq('type', 'URL'), editRedirection ],
[ R.T, editZoneRecord ],
])));
const updateHosts = async hosts => {
const remoteHostList = await getHosts();
const { add, edit } = diffRecords(remoteHostList, hosts);
return Promise.all([ addRecords(add), editRecords(edit) ]);
};
return { getHosts, setHosts: addRecords, updateHosts };
};
const domainService = getDomainService({ cpanel }); const domainService = getDomainService({ cpanel });
module.exports = { module.exports = {
+26 -18
View File
@@ -34,27 +34,35 @@ const CpanelClient = (options) => {
api({ basePath: 'execute', action: `${module}/${func}` })(module, func, defaultQuery); api({ basePath: 'execute', action: `${module}/${func}` })(module, func, defaultQuery);
return { return {
// { customonly, domain } zone: {
// -> { cpanelresult: { data[{ class, ttl, name, line, Line, cname, type, record }] } } // { customonly, domain }
fetchZoneRecords: R.compose( // -> { cpanelresult: { data[{ class, ttl, name, line, Line, cname, type, record }] } }
p => p.then(R.pathOr([], ['cpanelresult', 'data'])), fetch: R.compose(
api2('ZoneEdit', 'fetchzone_records', { customonly: 1, domain: options.domain }) p => p.then(R.pathOr([], ['cpanelresult', 'data'])),
), api2('ZoneEdit', 'fetchzone_records', { customonly: 1, domain: options.domain })
),
// { domain, name, type(A|CNAME), cname, address, ttl } // { name, type(A|CNAME), cname, address, ttl }
// -> {} // -> {}
addZoneRecord: api2('ZoneEdit', 'add_zone_record', { domain: options.domain }), add: api2('ZoneEdit', 'add_zone_record', { domain: options.domain }),
// {} // { name, type(A|CNAME), cname, address, ttl }
// -> { domain, destination } // -> {}
fetchRedirections: R.compose( edit: api2('ZoneEdit', 'edit_zone_record', { domain: options.domain }),
p => p.then(R.pathOr([], ['data'])), },
uapi('Mime', 'list_redirects'), redirection: {
), // {}
// -> { domain, destination }
fetch: R.compose(
p => p.then(R.pathOr([], ['data'])),
uapi('Mime', 'list_redirects'),
),
// { domain, redirect, type(permanent|tmp), redirect_wildcard(0|1), redirect(0|1|2) } // { domain, redirect, type(permanent|tmp), redirect_wildcard(0|1), redirect(0|1|2) }
// -> {} // -> {}
addRedirection: uapi('Mime', 'add_redirect'), add: uapi('Mime', 'add_redirect'),
edit: uapi('Mime', 'add_redirect'), // NOTE: adding new updates exisiting
},
}; };
}; };