fix: zone resolution logic
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@
|
||||
"!dist/**/*.test.*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"build": "tsc -p tsconfig.build.json && chmod +x dist/cli.js",
|
||||
"build:watch": "tsc -p tsconfig.build.json --watch",
|
||||
"clean": "rimraf dist",
|
||||
"lint": "tsc --noEmit",
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
export interface Config {
|
||||
keyVaultUrl: string;
|
||||
keyVaultUrl?: string;
|
||||
acmeDirectoryUrl: string;
|
||||
acmeContactEmail: string;
|
||||
acmeContactEmail?: string;
|
||||
subscriptionId: string;
|
||||
resourceGroups: string[];
|
||||
dnsZones?: string[];
|
||||
@@ -54,12 +54,12 @@ export function loadConfig(): Config {
|
||||
}
|
||||
|
||||
return {
|
||||
keyVaultUrl: requireEnv('ACME_KEYVAULT_URL'),
|
||||
keyVaultUrl: process.env['ACME_KEYVAULT_URL'],
|
||||
acmeDirectoryUrl: optionalEnv(
|
||||
'ACME_DIRECTORY_URL',
|
||||
'https://acme-v02.api.letsencrypt.org/directory'
|
||||
),
|
||||
acmeContactEmail: requireEnv('ACME_CONTACT_EMAIL'),
|
||||
acmeContactEmail: process.env['ACME_CONTACT_EMAIL'],
|
||||
subscriptionId: requireEnv('ACME_SUBSCRIPTION_ID'),
|
||||
resourceGroups,
|
||||
dnsZones,
|
||||
|
||||
+35
-14
@@ -63,13 +63,14 @@ function isAcmeTagged(tags: Record<string, string> | undefined): boolean {
|
||||
|
||||
export class DnsChallengeManager {
|
||||
private readonly client: DnsManagementClient;
|
||||
private zoneMap: Map<string, string> | undefined; // zone name → resource group
|
||||
|
||||
constructor(credential: TokenCredential, private readonly config: Config) {
|
||||
this.client = new DnsManagementClient(credential, config.subscriptionId);
|
||||
}
|
||||
|
||||
async createTxtRecord(fqdn: string, value: string): Promise<void> {
|
||||
const { resourceGroup, zone, name } = this.parseFqdn(fqdn);
|
||||
const { resourceGroup, zone, name } = await this.resolveFqdn(fqdn);
|
||||
await this.client.recordSets.createOrUpdate(resourceGroup, zone, name, 'TXT', {
|
||||
ttl: this.config.dnsChallengeTtl,
|
||||
txtRecords: [{ value: [value] }],
|
||||
@@ -77,7 +78,7 @@ export class DnsChallengeManager {
|
||||
}
|
||||
|
||||
async deleteTxtRecord(fqdn: string): Promise<void> {
|
||||
const { resourceGroup, zone, name } = this.parseFqdn(fqdn);
|
||||
const { resourceGroup, zone, name } = await this.resolveFqdn(fqdn);
|
||||
try {
|
||||
await this.client.recordSets.delete(resourceGroup, zone, name, 'TXT');
|
||||
} catch {
|
||||
@@ -85,20 +86,40 @@ export class DnsChallengeManager {
|
||||
}
|
||||
}
|
||||
|
||||
private parseFqdn(fqdn: string): { resourceGroup: string; zone: string; name: string } {
|
||||
private async loadZoneMap(): Promise<Map<string, string>> {
|
||||
if (this.zoneMap) return this.zoneMap;
|
||||
|
||||
this.zoneMap = new Map();
|
||||
for (const rg of this.config.resourceGroups) {
|
||||
for (const zone of this.config.dnsZones ?? []) {
|
||||
if (fqdn.endsWith(`.${zone}`) || fqdn === zone) {
|
||||
const name = fqdn === zone ? '@' : fqdn.slice(0, -(zone.length + 1));
|
||||
return { resourceGroup: rg, zone, name };
|
||||
}
|
||||
for await (const zone of this.client.zones.listByResourceGroup(rg)) {
|
||||
if (!zone.name) continue;
|
||||
if (this.config.dnsZones && !this.config.dnsZones.includes(zone.name)) continue;
|
||||
this.zoneMap.set(zone.name, rg);
|
||||
}
|
||||
}
|
||||
// fallback: derive zone from fqdn by stripping first label
|
||||
const parts = fqdn.split('.');
|
||||
const zone = parts.slice(1).join('.');
|
||||
const name = parts[0];
|
||||
const rg = this.config.resourceGroups[0];
|
||||
return { resourceGroup: rg, zone, name };
|
||||
return this.zoneMap;
|
||||
}
|
||||
|
||||
private async resolveFqdn(fqdn: string): Promise<{ resourceGroup: string; zone: string; name: string }> {
|
||||
const zones = await this.loadZoneMap();
|
||||
|
||||
// longest-suffix match: find the most specific zone that is a suffix of fqdn
|
||||
let bestZone = '';
|
||||
for (const zoneName of zones.keys()) {
|
||||
if (
|
||||
(fqdn === zoneName || fqdn.endsWith(`.${zoneName}`)) &&
|
||||
zoneName.length > bestZone.length
|
||||
) {
|
||||
bestZone = zoneName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bestZone) {
|
||||
throw new Error(`No Azure DNS zone found for FQDN: ${fqdn}`);
|
||||
}
|
||||
|
||||
const resourceGroup = zones.get(bestZone)!;
|
||||
const name = fqdn === bestZone ? '@' : fqdn.slice(0, -(bestZone.length + 1));
|
||||
return { resourceGroup, zone: bestZone, name };
|
||||
}
|
||||
}
|
||||
|
||||
+30
-6
@@ -15,18 +15,42 @@ export interface ProvisioningResult {
|
||||
|
||||
export class Provisioner {
|
||||
private readonly credential: DefaultAzureCredential;
|
||||
private readonly store: KeyVaultStore;
|
||||
private readonly acme: AcmeClient;
|
||||
private readonly challengeManager: DnsChallengeManager;
|
||||
private _store: KeyVaultStore | undefined;
|
||||
private _acme: AcmeClient | undefined;
|
||||
private _challengeManager: DnsChallengeManager | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly log: (msg: string, ...args: unknown[]) => void = console.log
|
||||
) {
|
||||
this.credential = new DefaultAzureCredential();
|
||||
this.store = new KeyVaultStore(this.credential, config.keyVaultUrl);
|
||||
this.acme = new AcmeClient(this.store, config, (msg) => this.log(msg));
|
||||
this.challengeManager = new DnsChallengeManager(this.credential, config);
|
||||
}
|
||||
|
||||
private get store(): KeyVaultStore {
|
||||
if (!this._store) {
|
||||
if (!this.config.keyVaultUrl) {
|
||||
throw new Error('ACME_KEYVAULT_URL is required for this operation');
|
||||
}
|
||||
this._store = new KeyVaultStore(this.credential, this.config.keyVaultUrl);
|
||||
}
|
||||
return this._store;
|
||||
}
|
||||
|
||||
private get acme(): AcmeClient {
|
||||
if (!this._acme) {
|
||||
if (!this.config.acmeContactEmail) {
|
||||
throw new Error('ACME_CONTACT_EMAIL is required for this operation');
|
||||
}
|
||||
this._acme = new AcmeClient(this.store, this.config, (msg) => this.log(msg));
|
||||
}
|
||||
return this._acme;
|
||||
}
|
||||
|
||||
private get challengeManager(): DnsChallengeManager {
|
||||
if (!this._challengeManager) {
|
||||
this._challengeManager = new DnsChallengeManager(this.credential, this.config);
|
||||
}
|
||||
return this._challengeManager;
|
||||
}
|
||||
|
||||
async run(dryRun = false): Promise<ProvisioningResult> {
|
||||
|
||||
Reference in New Issue
Block a user