Implement PowerDNS provider #1
106
PLAN.md
Normal file
106
PLAN.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Plan: Adding PowerDNS as a DNS Provider to Cloudron
|
||||
|
||||
This document outlines the detailed steps required to add support for PowerDNS via the PowerDNS Authoritative Server REST API to Cloudron. It covers frontend UI updates, backend DNS provider implementation, unit testing, and live testing instructions.
|
||||
|
||||
## 1. Frontend Modifications
|
||||
|
||||
The Cloudron dashboard is a Vue application that needs to know about the new provider and how to prompt the user for credentials.
|
||||
|
||||
### `dashboard/src/models/DomainsModel.js`
|
||||
* **Add Provider:** Append `{ name: 'PowerDNS', value: 'powerdns' }` to the `providers` array.
|
||||
* **Define Required Properties:** In the `getProviderConfigProps(provider)` switch statement, add a case for PowerDNS to define the fields it needs:
|
||||
```javascript
|
||||
case 'powerdns':
|
||||
props = ['apiUrl', 'apiKey'];
|
||||
break;
|
||||
```
|
||||
|
||||
### `dashboard/src/components/DomainProviderForm.vue`
|
||||
* **Reset Logic:** In the `dnsConfig` reset block (around line 57), add `dnsConfig.value.apiUrl = '';` to ensure the form clears properly.
|
||||
* **UI Template:** Add the HTML form groups for PowerDNS within the template section:
|
||||
```html
|
||||
<!-- powerdns -->
|
||||
<FormGroup v-if="provider === 'powerdns'">
|
||||
<label for="powerdnsApiUrlInput">{{ $t('domains.domainDialog.powerdnsApiUrl') }}</label>
|
||||
<TextInput id="powerdnsApiUrlInput" type="url" v-model="dnsConfig.apiUrl" placeholder="https://ns1.example.com:8081" required />
|
||||
</FormGroup>
|
||||
<FormGroup v-if="provider === 'powerdns'">
|
||||
<label for="powerdnsApiKeyInput">{{ $t('domains.domainDialog.powerdnsApiKey') }}</label>
|
||||
<MaskedInput id="powerdnsApiKeyInput" v-model="dnsConfig.apiKey" required />
|
||||
</FormGroup>
|
||||
```
|
||||
|
||||
### `dashboard/public/translation/en.json`
|
||||
* **Add Translations:** Add the English translation strings for the new fields under the `domains.domainDialog` object:
|
||||
```json
|
||||
"powerdnsApiUrl": "PowerDNS API URL (e.g., https://ns1.example.com:8081)",
|
||||
"powerdnsApiKey": "API Key",
|
||||
```
|
||||
|
||||
## 2. Backend Modifications
|
||||
|
||||
The backend needs a new driver file that implements the standard Cloudron DNS provider interface.
|
||||
|
||||
### `src/dns.js`
|
||||
* **Import the Driver:** Add `import dnsPowerdns from './dns/powerdns.js';` at the top with the other imports.
|
||||
* **Register the Provider:** Add `powerdns: dnsPowerdns` to the `DNS_PROVIDERS` mapping object.
|
||||
|
||||
### `src/dns/powerdns.js` (New File)
|
||||
Create a new file that implements the standard DNS interface (`src/dns/interface.js`).
|
||||
* **Required Methods:** Export `removePrivateFields`, `injectPrivateFields`, `upsert`, `get`, `del`, `wait`, and `verifyDomainConfig`.
|
||||
* **API Interactions:**
|
||||
* Construct the base URL: `const baseUrl = domainConfig.apiUrl.replace(/\/$/, '');`
|
||||
* Ensure queries and zone names have a trailing dot, as PowerDNS strictly requires absolute FQDNs (e.g., `example.com.`).
|
||||
* Set the `X-API-Key` header using `domainConfig.apiKey` for all requests.
|
||||
* **Operations:**
|
||||
* `get`: Use `GET ${baseUrl}/api/v1/servers/localhost/zones/${zoneName}.` and extract records from the `rrsets` array matching the exact `fqdn` and `type`.
|
||||
* `upsert`: Use `PATCH ${baseUrl}/api/v1/servers/localhost/zones/${zoneName}.`. Send an `rrsets` payload with `changetype: 'REPLACE'`. Ensure `TXT` values are quoted.
|
||||
* `del`: Use `PATCH` with `changetype: 'DELETE'`.
|
||||
* **verifyDomainConfig:** Perform a test `upsert` and `del` of an `A` record on the subdomain `cloudrontestdns` to ensure the API is reachable and the key has edit permissions.
|
||||
|
||||
## 3. Automated Testing (Local)
|
||||
|
||||
The new provider needs to be verified against the existing test suite using mock HTTP requests.
|
||||
|
||||
### `src/test/dns-providers-test.js`
|
||||
* Add a new `describe('powerdns', ...)` block.
|
||||
* Setup mock domain configuration with a dummy `apiUrl` and `apiKey`.
|
||||
* Use `nock` to intercept requests to the dummy `apiUrl`.
|
||||
* Write tests for:
|
||||
* `upsert` non-existing and existing records.
|
||||
* `get` records.
|
||||
* `del` records.
|
||||
* Ensure the nock endpoints expect the exact JSON structure and `rrsets` payload required by PowerDNS.
|
||||
|
||||
### Running the Tests
|
||||
Run the specific test suite for DNS providers locally:
|
||||
```bash
|
||||
./run-tests src/test/dns-providers-test.js
|
||||
```
|
||||
*(This uses the fast-path to run mocha directly, skipping the full Cloudron test environment setup).*
|
||||
|
||||
## 4. Live Testing (Remote Cloudron Server)
|
||||
|
||||
To test the integration end-to-end, deploy the changes to a temporary Cloudron instance using the provided hotfix script.
|
||||
|
||||
**Important Considerations for Hotfixing:**
|
||||
The `scripts/hotfix` tool builds a release tarball by executing `git archive HEAD`. **You must commit your changes locally before running the hotfix**, otherwise the script will fail or push the old codebase.
|
||||
|
||||
1. **Commit Changes:**
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Implement PowerDNS provider"
|
||||
```
|
||||
|
||||
2. **Run Hotfix Script:**
|
||||
Execute the hotfix script from the repository root, pointing it to your test server.
|
||||
```bash
|
||||
./scripts/hotfix --cloudron <IP_OR_DOMAIN_OF_TEST_SERVER> --ssh-user root --ssh-key ~/.ssh/id_rsa --release 1.0.0-test
|
||||
```
|
||||
|
||||
3. **Verify Deployment:**
|
||||
* The script will install dependencies, compile the Vue dashboard, bundle the backend code, push it to the server, and restart the `box` service.
|
||||
* Log into the Cloudron dashboard via your browser.
|
||||
* Navigate to Domains -> Add Domain, select "PowerDNS".
|
||||
* Enter a test domain, a valid PowerDNS API URL, and an API Key.
|
||||
* Verify that Cloudron successfully configures the domain and creates the necessary initial DNS records.
|
||||
@@ -825,6 +825,8 @@
|
||||
"domain": "Domain",
|
||||
"provider": "DNS provider",
|
||||
"route53AccessKeyId": "Access key ID",
|
||||
"powerdnsApiUrl": "PowerDNS API URL (e.g., https://ns1.example.com:8081)",
|
||||
"powerdnsApiKey": "API Key",
|
||||
"route53SecretAccessKey": "Secret access key",
|
||||
"gcdnsServiceAccountKey": "Service account key",
|
||||
"digitalOceanToken": "DigitalOcean token",
|
||||
|
||||
@@ -56,6 +56,7 @@ function needsPort80(dnsProvider, tlsProvider) {
|
||||
function resetFields() {
|
||||
dnsConfig.value.accessKeyId = '';
|
||||
dnsConfig.value.accessKey = '';
|
||||
dnsConfig.value.apiUrl = '';
|
||||
dnsConfig.value.accessToken = '';
|
||||
dnsConfig.value.apiKey = '';
|
||||
dnsConfig.value.appKey = '';
|
||||
@@ -134,6 +135,16 @@ function onGcdnsFileInputChange(event) {
|
||||
<div class="warning-label" v-show="provider === 'manual'" v-html="$t('domains.domainDialog.manualInfo')"></div>
|
||||
<div class="warning-label" v-show="needsPort80(provider, tlsProvider)" v-html="$t('domains.domainDialog.letsEncryptInfo')"></div>
|
||||
|
||||
<!-- powerdns -->
|
||||
<FormGroup v-if="provider === 'powerdns'">
|
||||
<label for="powerdnsApiUrlInput">{{ $t('domains.domainDialog.powerdnsApiUrl') }}</label>
|
||||
<TextInput id="powerdnsApiUrlInput" type="url" v-model="dnsConfig.apiUrl" placeholder="https://ns1.example.com:8081" required />
|
||||
</FormGroup>
|
||||
<FormGroup v-if="provider === 'powerdns'">
|
||||
<label for="powerdnsApiKeyInput">{{ $t('domains.domainDialog.powerdnsApiKey') }}</label>
|
||||
<MaskedInput id="powerdnsApiKeyInput" v-model="dnsConfig.apiKey" required />
|
||||
</FormGroup>
|
||||
|
||||
<!-- Route53 -->
|
||||
<FormGroup v-if="provider === 'route53'">
|
||||
<label for="accessKeyIdInput">{{ $t('domains.domainDialog.route53AccessKeyId') }}</label>
|
||||
|
||||
@@ -23,6 +23,7 @@ const providers = [
|
||||
{ name: 'Porkbun', value: 'porkbun' },
|
||||
{ name: 'Vultr', value: 'vultr' },
|
||||
{ name: 'Wildcard', value: 'wildcard' },
|
||||
{ name: 'PowerDNS', value: 'powerdns' },
|
||||
{ name: 'Manual (not recommended)', value: 'manual' },
|
||||
{ name: 'No-op (only for development)', value: 'noop' }
|
||||
];
|
||||
@@ -90,6 +91,9 @@ function filterConfigForProvider(provider, config) {
|
||||
case 'porkbun':
|
||||
props = ['apikey', 'secretapikey'];
|
||||
break;
|
||||
case 'powerdns':
|
||||
props = ['apiUrl', 'apiKey'];
|
||||
break;
|
||||
}
|
||||
|
||||
const ret = {
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -364,7 +364,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1000.0.tgz",
|
||||
"integrity": "sha512-7kPy33qNGq3NfwHC0412T6LDK1bp4+eiPzetX0sVd9cpTSXuQDKpoOFnB0Njj6uZjJDcLS3n2OeyarwwgkQ0Ow==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha1-browser": "5.2.0",
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
@@ -1229,7 +1228,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
@@ -1268,7 +1266,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
@@ -3133,7 +3130,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz",
|
||||
"integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
@@ -3222,7 +3218,6 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3379,7 +3374,6 @@
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*"
|
||||
},
|
||||
@@ -4573,7 +4567,6 @@
|
||||
"integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
@@ -6197,7 +6190,6 @@
|
||||
"resolved": "https://registry.npmjs.org/koa/-/koa-3.1.1.tgz",
|
||||
"integrity": "sha512-KDDuvpfqSK0ZKEO2gCPedNjl5wYpfj+HNiuVRlbhd1A88S3M0ySkdf2V/EJ4NWt5dwh5PXCdcenrKK2IQJAxsg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^1.3.8",
|
||||
"content-disposition": "~0.5.4",
|
||||
|
||||
@@ -34,6 +34,7 @@ import dnsNoop from './dns/noop.js';
|
||||
import dnsManual from './dns/manual.js';
|
||||
import dnsOvh from './dns/ovh.js';
|
||||
import dnsPorkbun from './dns/porkbun.js';
|
||||
import dnsPowerdns from './dns/powerdns.js';
|
||||
import dnsWildcard from './dns/wildcard.js';
|
||||
|
||||
const { log } = logger('dns');
|
||||
@@ -45,7 +46,7 @@ const DNS_PROVIDERS = {
|
||||
godaddy: dnsGodaddy, inwx: dnsInwx, linode: dnsLinode, vultr: dnsVultr,
|
||||
namecom: dnsNamecom, namecheap: dnsNamecheap, netcup: dnsNetcup, hetzner: dnsHetzner,
|
||||
hetznercloud: dnsHetznercloud, noop: dnsNoop, manual: dnsManual, ovh: dnsOvh,
|
||||
porkbun: dnsPorkbun, wildcard: dnsWildcard
|
||||
porkbun: dnsPorkbun, powerdns: dnsPowerdns, wildcard: dnsWildcard
|
||||
};
|
||||
|
||||
// choose which subdomain backend we use for test purpose we use route53
|
||||
|
||||
165
src/dns/powerdns.js
Normal file
165
src/dns/powerdns.js
Normal file
@@ -0,0 +1,165 @@
|
||||
import assert from 'node:assert';
|
||||
import BoxError from '../boxerror.js';
|
||||
import logger from '../logger.js';
|
||||
import dns from '../dns.js';
|
||||
import safe from '@cloudron/safetydance';
|
||||
import superagent from '@cloudron/superagent';
|
||||
import waitForDns from './waitfordns.js';
|
||||
|
||||
const { log } = logger('dns/powerdns');
|
||||
|
||||
function formatError(response) {
|
||||
return `PowerDNS error ${response.status} ${response.body ? JSON.stringify(response.body) : response.text}`;
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
delete domainObject.config.apiKey;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (!Object.hasOwn(newConfig, 'apiKey')) newConfig.apiKey = currentConfig.apiKey;
|
||||
}
|
||||
|
||||
async function get(domainObject, subdomain, type) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
|
||||
const domainConfig = domainObject.config;
|
||||
const baseUrl = domainConfig.apiUrl.replace(/\/$/, '');
|
||||
const zoneName = domainObject.zoneName + '.';
|
||||
const fqdn = dns.fqdn(subdomain, domainObject.domain) + '.';
|
||||
|
||||
log(`get: domain ${domainObject.domain} zone ${zoneName} fqdn ${fqdn} type ${type}`);
|
||||
|
||||
const [error, response] = await safe(superagent.get(`${baseUrl}/api/v1/servers/localhost/zones/${zoneName}`)
|
||||
.set('X-API-Key', domainConfig.apiKey)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.ok(() => true));
|
||||
|
||||
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
||||
if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND, formatError(response));
|
||||
if (response.status === 401 || response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
|
||||
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
||||
|
||||
const rrset = response.body.rrsets.find(r => r.name === fqdn && r.type === type);
|
||||
if (!rrset) return [];
|
||||
|
||||
return rrset.records.map(r => {
|
||||
if (type === 'TXT') return r.content.replace(/^"(.*)"$/, '$1');
|
||||
return r.content;
|
||||
});
|
||||
}
|
||||
|
||||
async function upsert(domainObject, subdomain, type, values) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(Array.isArray(values));
|
||||
|
||||
const domainConfig = domainObject.config;
|
||||
const baseUrl = domainConfig.apiUrl.replace(/\/$/, '');
|
||||
const zoneName = domainObject.zoneName + '.';
|
||||
const fqdn = dns.fqdn(subdomain, domainObject.domain) + '.';
|
||||
|
||||
log(`upsert: domain ${domainObject.domain} zone ${zoneName} fqdn ${fqdn} type ${type} values ${values}`);
|
||||
|
||||
const records = values.map(v => {
|
||||
let content = v;
|
||||
if (type === 'TXT' && !content.startsWith('"')) content = `"${v}"`;
|
||||
return { content, disabled: false };
|
||||
});
|
||||
|
||||
const rrset = {
|
||||
name: fqdn,
|
||||
type: type,
|
||||
ttl: 60,
|
||||
changetype: 'REPLACE',
|
||||
records: records
|
||||
};
|
||||
|
||||
const [error, response] = await safe(superagent.patch(`${baseUrl}/api/v1/servers/localhost/zones/${zoneName}`)
|
||||
.set('X-API-Key', domainConfig.apiKey)
|
||||
.send({ rrsets: [rrset] })
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.ok(() => true));
|
||||
|
||||
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
||||
if (response.status === 401 || response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
|
||||
if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
||||
}
|
||||
|
||||
async function del(domainObject, subdomain, type, values) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(Array.isArray(values));
|
||||
|
||||
const domainConfig = domainObject.config;
|
||||
const baseUrl = domainConfig.apiUrl.replace(/\/$/, '');
|
||||
const zoneName = domainObject.zoneName + '.';
|
||||
const fqdn = dns.fqdn(subdomain, domainObject.domain) + '.';
|
||||
|
||||
log(`del: domain ${domainObject.domain} zone ${zoneName} fqdn ${fqdn} type ${type} values ${values}`);
|
||||
|
||||
const rrset = {
|
||||
name: fqdn,
|
||||
type: type,
|
||||
changetype: 'DELETE'
|
||||
};
|
||||
|
||||
const [error, response] = await safe(superagent.patch(`${baseUrl}/api/v1/servers/localhost/zones/${zoneName}`)
|
||||
.set('X-API-Key', domainConfig.apiKey)
|
||||
.send({ rrsets: [rrset] })
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.ok(() => true));
|
||||
|
||||
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
|
||||
if (response.status === 401 || response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
|
||||
if (response.status === 404 || response.status === 422) return;
|
||||
if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
|
||||
}
|
||||
|
||||
async function wait(domainObject, subdomain, type, value, options) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(options && typeof options === 'object');
|
||||
|
||||
const fqdn = dns.fqdn(subdomain, domainObject.domain);
|
||||
await waitForDns(fqdn, domainObject.zoneName, type, value, options);
|
||||
}
|
||||
|
||||
async function verifyDomainConfig(domainObject) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
|
||||
const domainConfig = domainObject.config;
|
||||
if (!domainConfig.apiUrl || typeof domainConfig.apiUrl !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'apiUrl must be a non-empty string');
|
||||
if (!domainConfig.apiKey || typeof domainConfig.apiKey !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'apiKey must be a non-empty string');
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
const testIp = '127.0.0.1';
|
||||
|
||||
await upsert(domainObject, testSubdomain, 'A', [testIp]);
|
||||
await del(domainObject, testSubdomain, 'A', [testIp]);
|
||||
|
||||
return {
|
||||
apiUrl: domainConfig.apiUrl,
|
||||
apiKey: domainConfig.apiKey
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
removePrivateFields,
|
||||
injectPrivateFields,
|
||||
upsert,
|
||||
get,
|
||||
del,
|
||||
wait,
|
||||
verifyDomainConfig
|
||||
};
|
||||
@@ -412,6 +412,79 @@ describe('dns provider', function () {
|
||||
const TOKEN = 'sometoken';
|
||||
const NAMECOM_API = 'https://api.name.com/v4';
|
||||
|
||||
|
||||
describe('powerdns', function () {
|
||||
const API_URL = 'http://ns1.example.com:8081';
|
||||
const API_KEY = 'secret';
|
||||
|
||||
before(async function () {
|
||||
domainCopy.provider = 'powerdns';
|
||||
domainCopy.config = {
|
||||
apiUrl: API_URL,
|
||||
apiKey: API_KEY
|
||||
};
|
||||
|
||||
await domains.setConfig(domainCopy.domain, domainCopy, auditSource);
|
||||
});
|
||||
|
||||
it('upsert non-existing record succeeds', async function () {
|
||||
nock.cleanAll();
|
||||
|
||||
const zoneName = domainCopy.zoneName + '.';
|
||||
const fqdn = 'test.' + domainCopy.domain + '.';
|
||||
|
||||
const req1 = nock(API_URL)
|
||||
.patch('/api/v1/servers/localhost/zones/' + zoneName, body => {
|
||||
return body.rrsets[0].name === fqdn &&
|
||||
body.rrsets[0].type === 'A' &&
|
||||
body.rrsets[0].changetype === 'REPLACE' &&
|
||||
body.rrsets[0].records[0].content === '1.2.3.4';
|
||||
})
|
||||
.reply(204);
|
||||
|
||||
await dns.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4']);
|
||||
assert.ok(req1.isDone());
|
||||
});
|
||||
|
||||
it('get succeeds', async function () {
|
||||
nock.cleanAll();
|
||||
|
||||
const zoneName = domainCopy.zoneName + '.';
|
||||
const fqdn = 'test.' + domainCopy.domain + '.';
|
||||
|
||||
const req1 = nock(API_URL)
|
||||
.get('/api/v1/servers/localhost/zones/' + zoneName)
|
||||
.reply(200, {
|
||||
rrsets: [{
|
||||
name: fqdn,
|
||||
type: 'A',
|
||||
records: [{ content: '1.2.3.4', disabled: false }]
|
||||
}]
|
||||
});
|
||||
|
||||
const result = await dns.getDnsRecords('test', domainCopy.domain, 'A');
|
||||
assert.deepEqual(result, ['1.2.3.4']);
|
||||
assert.ok(req1.isDone());
|
||||
});
|
||||
|
||||
it('del succeeds', async function () {
|
||||
nock.cleanAll();
|
||||
|
||||
const zoneName = domainCopy.zoneName + '.';
|
||||
const fqdn = 'test.' + domainCopy.domain + '.';
|
||||
|
||||
const req1 = nock(API_URL)
|
||||
.patch('/api/v1/servers/localhost/zones/' + zoneName, body => {
|
||||
return body.rrsets[0].name === fqdn &&
|
||||
body.rrsets[0].type === 'A' &&
|
||||
body.rrsets[0].changetype === 'DELETE';
|
||||
})
|
||||
.reply(204);
|
||||
|
||||
await dns.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4']);
|
||||
assert.ok(req1.isDone());
|
||||
});
|
||||
});
|
||||
before(async function () {
|
||||
domainCopy.provider = 'namecom';
|
||||
domainCopy.config = {
|
||||
|
||||
Reference in New Issue
Block a user