feat: DNS provider abstraction — Technitium, Cloudflare, RFC 2136, Manual
- dns-providers/: adapter base class + registry with auto-discovery - technitium.js: wraps existing Technitium API calls into adapter interface - cloudflare.js: Cloudflare API v4 adapter (zones, records, credentials) - rfc2136.js: RFC 2136 dynamic DNS via nsupdate (BIND, PowerDNS, etc.) - manual.js: no-op adapter for external DNS management with instructions - provider-dns.js: provider-aware DNS context, resolves active adapter from config - Universal helper methods: universalCreateRecord/Delete/ResolveRecord - All 7 route files updated to use universal methods instead of raw dns.call() - Setup wizard: provider dropdown (Technitium, Cloudflare, RFC 2136, Manual) - DNS template selector: added Cloudflare and External/Manual options - Config schema: validates dns.provider field - Capability gating on Technitium-specific endpoints (logs, restart, update) - Backward compatible: no provider set = auto-detect (technitium if dns.ip exists)
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* DNS Provider Registry
|
||||
* Manages available DNS provider adapters.
|
||||
* Providers register themselves, and the active provider is selected by config.
|
||||
*/
|
||||
const path = require('path');
|
||||
|
||||
class DNSProviderRegistry {
|
||||
constructor() {
|
||||
this.providers = new Map(); // providerId -> adapter class
|
||||
this.instances = new Map(); // providerId -> adapter instance
|
||||
}
|
||||
|
||||
/** Register a provider adapter class */
|
||||
register(adapterClass) {
|
||||
const instance = new adapterClass({}, {});
|
||||
const id = instance.providerId;
|
||||
if (this.providers.has(id)) {
|
||||
console.warn(`DNS provider "${id}" already registered, overwriting`);
|
||||
}
|
||||
this.providers.set(id, adapterClass);
|
||||
}
|
||||
|
||||
/** Get list of all registered provider IDs */
|
||||
getProviderIds() {
|
||||
return Array.from(this.providers.keys());
|
||||
}
|
||||
|
||||
/** Get metadata for all providers (without instantiating with real config) */
|
||||
getProviderMeta() {
|
||||
return this.getProviderIds().map(id => {
|
||||
const Adapter = this.providers.get(id);
|
||||
const inst = new Adapter({}, {});
|
||||
return {
|
||||
id: inst.providerId,
|
||||
displayName: inst.displayName,
|
||||
capabilities: inst.getCapabilities()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create an adapter instance for the given provider + config
|
||||
* @param {string} providerId - The provider to instantiate
|
||||
* @param {Object} config - Provider-specific configuration
|
||||
* @param {Object} ctx - Shared application context
|
||||
* @returns {BaseDNSProvider} The provider adapter instance
|
||||
*/
|
||||
getProvider(providerId, config, ctx) {
|
||||
// Re-create if config changed
|
||||
const cacheKey = providerId;
|
||||
const Adapter = this.providers.get(providerId);
|
||||
if (!Adapter) {
|
||||
throw new Error(`Unknown DNS provider: ${providerId}. Available: ${this.getProviderIds().join(', ')}`);
|
||||
}
|
||||
const instance = new Adapter(config, ctx);
|
||||
this.instances.set(cacheKey, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/** Auto-discover and register all providers in this directory */
|
||||
autoDiscover() {
|
||||
const fs = require('fs');
|
||||
const dir = __dirname;
|
||||
const files = fs.readdirSync(dir).filter(f =>
|
||||
f !== 'base.js' && f !== 'registry.js' && f.endsWith('.js') && !f.startsWith('.')
|
||||
);
|
||||
for (const file of files) {
|
||||
try {
|
||||
const Loaded = require(path.join(dir, file));
|
||||
// Support: module.exports = Class, module.exports = { Class }, or plain objects
|
||||
let cls = null;
|
||||
if (typeof Loaded === 'function') {
|
||||
cls = Loaded;
|
||||
} else if (typeof Loaded === 'object' && Loaded !== null) {
|
||||
// Try to find a class in the exported object
|
||||
cls = Object.values(Loaded).find(v => typeof v === 'function');
|
||||
}
|
||||
if (cls) {
|
||||
// Verify it has providerId (on prototype or set in constructor)
|
||||
try {
|
||||
const test = new cls({}, {});
|
||||
if (test.providerId && typeof test.getCapabilities === 'function') {
|
||||
this.register(cls);
|
||||
}
|
||||
} catch {
|
||||
// Not a valid provider adapter, skip
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to load DNS provider from ${file}:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton
|
||||
const registry = new DNSProviderRegistry();
|
||||
registry.autoDiscover();
|
||||
|
||||
module.exports = registry;
|
||||
Reference in New Issue
Block a user