diff --git a/README.md b/README.md index a276a84..bb96bce 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Validation class for objects in NicTool. Analgous to Nictool/../\*/Sanity in v2. ✔ rejects invalid: a.b d.com. ✔ rejects invalid: a.b d.com. - export_type + type ✔ rejects missing ✔ accepts valid: bind ✔ accepts valid: djbdns diff --git a/lib/nameserver.js b/lib/nameserver.js index 9230fcb..3d4725b 100644 --- a/lib/nameserver.js +++ b/lib/nameserver.js @@ -10,16 +10,42 @@ export const name = Joi.string() .domain({ allowFullyQualified: true, tlds: false }) .pattern(/\.$/) -export const type = Joi.string().valid( +/** + * Which nameserver software this is — the `nt_nameserver_export_type` names, + * which is also what NameserverSupervisor dispatches on. + */ +export const BUILDABLE = [ 'bind', 'djbdns', 'knot', 'nsd', 'maradns', 'powerdns', - 'dynect', 'native', -) + 'coredns', +] + +/** + * 2.x export types that name how a nameserver is fed rather than a different + * nameserver, resolved to the type that implements them. + * + * bind-nsupdate BIND fed over the network (RFC 2136) instead of by file + * copy. 2.x made that an export type of its own; v3 makes it + * a transport choice, so the nameserver is just bind. + */ +export const ALIASES = { 'bind-nsupdate': 'bind' } + +/** + * A type a 2.x install can hold that nothing here implements. Storing one is + * allowed — an adopted 2.x record must survive a round trip through the API — + * but the supervisor refuses to start it rather than inventing a substitute. + */ +export const UNBUILDABLE = ['dynect'] + +/** The canonical spelling for a stored type, which may be a 2.x alias. */ +export const resolveType = (value) => ALIASES[value] ?? value + +export const type = Joi.string().valid(...BUILDABLE, ...Object.keys(ALIASES), ...UNBUILDABLE) export const remote_login = Joi.string().empty('').max(127) @@ -34,19 +60,29 @@ export const listen = Joi.array().items( ) export const publisher = Joi.object({ - type: Joi.string().valid('memory', 'rfc1035', 'tinydns-cdb', 'powerdns-db').required(), + type: Joi.string() + .valid('memory', 'rfc1035', 'maradns', 'tinydns-cdb', 'powerdns-db', 'coredns-redis', 'none') + .required(), path: Joi.string().empty('').max(1024), database: Joi.string().empty('').max(255), }).unknown(true) +// these objects are stored as JSON and read by the dns-nameserver classes export const transport = Joi.object({ - type: Joi.string().valid('noop', 'axfr', 'rsync', 'db-replication').required(), + type: Joi.string().valid('noop', 'axfr', 'rsync', 'db-replication', 'pull').required(), + // pull: free text naming how the far side fetches, e.g. "fetchzone from cron" + source: Joi.string().empty('').max(255), interval: shared.uint32.default(300), cooldown: shared.uint16.default(5), + // axfr: who to NOTIFY. "host", "host:port" and "[v6]:port" all parse. + notify: Joi.array().items(Joi.string().max(255)), master: Joi.string().empty('').max(255), - tsig_key: Joi.string().empty('').max(255), + tsigKey: Joi.string().empty('').max(255), + port: shared.uint16.min(1), + timeoutMs: shared.uint32, + attempts: shared.uint16.min(1), remote: Joi.string().empty('').max(255), - ssh_key: Joi.string().empty('').max(1024), + sshKey: Joi.string().empty('').max(1024), }).unknown(true) export const dnssec = Joi.object({ @@ -74,7 +110,7 @@ export const v3 = Joi.object({ remote_login: remote_login, logdir: Joi.string().empty('').max(255), datadir: Joi.string().empty('').min(2).max(255), - engine: type, + type: type.required(), listen: listen, publisher: publisher, transport: transport, @@ -83,7 +119,6 @@ export const v3 = Joi.object({ interval: shared.uint16, serials: Joi.boolean(), status: Joi.string().empty('').max(255), - type: type.required(), }), deleted: Joi.boolean(), }) @@ -104,7 +139,7 @@ export const PUT = Joi.object({ address: shared.ipv4, address6: shared.ipv6.empty(''), remote_login: remote_login, - engine: type, + type: type, listen: listen, publisher: publisher, transport: transport, @@ -113,7 +148,6 @@ export const PUT = Joi.object({ interval: shared.uint16, serials: Joi.boolean(), status: Joi.string().empty('').max(255), - type: type, }), deleted: Joi.boolean(), }) diff --git a/lib/nameserver.test.js b/lib/nameserver.test.js index 0e56376..f752d45 100644 --- a/lib/nameserver.test.js +++ b/lib/nameserver.test.js @@ -67,21 +67,25 @@ describe('nameserver', function () { } }) - describe('export.type', function () { + describe('type', function () { it(`rejects missing`, () => { const testCase = JSON.parse(JSON.stringify(testNS)) - delete testCase.export.type + delete testCase.type const { error, value } = schema.validate(testCase) - assert.strictEqual(error.message, '"export.type" is required') + assert.strictEqual(error.message, '"type" is required') assert.deepEqual(value, testCase) }) - for (const n of ['bind', 'djbdns', 'knot', 'nsd', 'maradns', 'powerdns', 'dynect', 'native']) { + for (const n of [ + ...nameserver.BUILDABLE, + ...Object.keys(nameserver.ALIASES), + ...nameserver.UNBUILDABLE, + ]) { it(`accepts valid: ${n}`, () => { const testCase = JSON.parse(JSON.stringify(testNS)) - testCase.export.type = n + testCase.type = n const { error, value } = schema.validate(testCase) @@ -90,17 +94,49 @@ describe('nameserver', function () { }) } + it('accepts coredns, which only v3 can build', () => { + const testCase = JSON.parse(JSON.stringify(testNS)) + testCase.type = 'coredns' + + assert.ifError(schema.validate(testCase).error) + }) + + // dynect has no v3 nameserver, but a 2.x install can hold one and the + // record has to survive a round trip. The supervisor refuses to start it. + it('accepts a 2.x type nothing here implements', () => { + const testCase = JSON.parse(JSON.stringify(testNS)) + testCase.type = 'dynect' + + assert.ifError(schema.validate(testCase).error) + }) + + // bind-nsupdate is BIND fed by RFC 2136 rather than by file copy, which is + // a transport choice here, not a separate nameserver. + it('resolves a 2.x alias to the type that implements it', () => { + assert.equal(nameserver.resolveType('bind-nsupdate'), 'bind') + }) + + it('leaves a canonical type alone', () => { + for (const t of nameserver.BUILDABLE) { + assert.equal(nameserver.resolveType(t), t) + } + }) + + it('does not claim an alias is buildable', () => { + for (const alias of Object.keys(nameserver.ALIASES)) { + assert.ok(!nameserver.BUILDABLE.includes(alias), alias) + assert.ok(nameserver.BUILDABLE.includes(nameserver.resolveType(alias)), alias) + } + }) + for (const n of ['cryptic', 'fuzzy', 'yitizg', 'bin', 'djbs', 'DJB', 'BIND', 'NT']) { it(`rejects invalid: ${n}`, () => { const testCase = JSON.parse(JSON.stringify(testNS)) - testCase.export.type = n + testCase.type = n const { error, value } = schema.validate(testCase) - assert.strictEqual( - error.message, - '"export.type" must be one of [bind, djbdns, knot, nsd, maradns, powerdns, dynect, native]', - ) + assert.match(error.message, /^"type" must be one of \[/) assert.deepEqual(value, testCase) }) } diff --git a/lib/test/nameserver.json b/lib/test/nameserver.json index d030553..8d11930 100644 --- a/lib/test/nameserver.json +++ b/lib/test/nameserver.json @@ -7,8 +7,8 @@ "remote_login": "nsd", "logdir": "/foo", "datadir": "/bar", + "type": "nsd", "export": { - "type": "nsd", "interval": 0, "serials": true, "status": "last run:03-05 15:25
last cp :09-20 12:59" diff --git a/package.json b/package.json index 74c77ec..07994ec 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ }, "homepage": "https://github.com/NicTool/validate#readme", "devDependencies": { - "eslint": "^10.7.0", + "eslint": "^10.8.0", "@eslint/js": "^10.0.1", "eslint-config-prettier": "^10.1.8", "globals": "^17.7.0"