perf: 添加Azure DNS插件支持及文档

添加Azure DNS插件实现,包括DNS记录管理和授权配置
新增Azure使用文档和配置截图
更新依赖添加@azure/arm-dns和@azure/identity包
This commit is contained in:
xiaojunnuo
2026-04-26 03:36:33 +08:00
parent edc7bfc230
commit 1f1d687317
15 changed files with 635 additions and 3 deletions
+2
View File
@@ -37,6 +37,8 @@
"pub": "echo 1"
},
"dependencies": {
"@azure/arm-dns": "^5.1.0",
"@azure/identity": "^4.13.1",
"@alicloud/fc20230330": "^4.1.7",
"@alicloud/openapi-client": "^0.4.12",
"@alicloud/openapi-util": "^0.3.2",
@@ -0,0 +1,268 @@
import { AccessInput, BaseAccess, IsAccess } from '@certd/pipeline';
import { DomainRecord } from '@certd/plugin-cert';
import { utils } from '@certd/basic';
import { PageRes, PageSearch } from '@certd/pipeline';
@IsAccess({
name: 'azure',
title: '微软云Azure授权',
desc: '',
icon: 'simple-icons:microsoftazure',
})
export class AzureAccess extends BaseAccess {
@AccessInput({
title: '订阅 ID',
component: {
placeholder: 'subscriptionId',
},
helper: 'Azure 订阅 ID',
required: true,
})
subscriptionId = '';
@AccessInput({
title: '资源组',
component: {
placeholder: 'resourceGroupName',
},
helper: 'DNS 区域所在的资源组名称',
required: true,
})
resourceGroupName = '';
@AccessInput({
title: '目录(租户) ID',
component: {
placeholder: 'tenantId',
},
helper: '目录(租户) ID',
required: true,
})
tenantId = '';
@AccessInput({
title: '应用程序ID',
component: {
placeholder: 'clientId',
},
helper: '应用程序(客户端) ID',
required: true,
})
clientId = '';
@AccessInput({
title: '客户端凭据',
component: {
placeholder: 'clientSecret',
},
required: true,
encrypt: true,
helper: '客户端凭据(机密)->客户端密码->新客户端密码->时间选长一点的->复制值',
})
clientSecret = '';
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "TestRequest"
},
helper: "测试授权是否正确"
})
testRequest = true;
async onTestRequest() {
this.ctx.logger.info('开始测试 Azure 认证...');
// 1. 先测试身份认证,获取访问令牌
const { ClientSecretCredential } = await import('@azure/identity');
const credential = new ClientSecretCredential(
this.tenantId,
this.clientId,
this.clientSecret
);
// 获取 Azure 管理 API 的访问令牌来验证凭据
this.ctx.logger.info('验证身份凭据...');
const token = await credential.getToken('https://management.azure.com/.default');
this.ctx.logger.info('身份认证成功!', token);
return "ok";
}
async getDnsManagementClient() {
const { DnsManagementClient } = await import('@azure/arm-dns');
const { ClientSecretCredential } = await import('@azure/identity');
const credential = new ClientSecretCredential(
this.tenantId,
this.clientId,
this.clientSecret
);
return new DnsManagementClient(credential, this.subscriptionId);
}
async listZones(): Promise<any[]> {
const client = await this.getDnsManagementClient();
const zones: any[] = [];
for await (const zone of client.zones.listByResourceGroup(this.resourceGroupName)) {
zones.push(zone);
}
return zones;
}
async getZoneId(domain: string): Promise<{ id: string; name: string }> {
const zones = await this.listZones();
const domainSuffix = domain.endsWith('.') ? domain : domain + '.';
const matchingZone = zones.find((zone: any) => {
const zoneName = zone.name.endsWith('.') ? zone.name : zone.name + '.';
return domainSuffix.endsWith(zoneName) || domainSuffix === zoneName;
});
if (!matchingZone) {
throw new Error(`找不到匹配的 DNS 区域: ${domain}`);
}
this.ctx.logger.info(`找到 DNS 区域: ${matchingZone.name}, ID: ${matchingZone.id}`);
return {
id: matchingZone.id.split('/').pop()!,
name: matchingZone.name,
};
}
async listZonesPage(req: PageSearch): Promise<PageRes<DomainRecord>> {
const zones = await this.listZones();
let list = zones;
if (req.searchKey) {
list = list.filter((zone: any) => zone.name.includes(req.searchKey));
}
list = list.map((item: any) => ({
id: item.id.split('/').pop()!,
domain: item.name,
}));
return {
total: list.length,
list,
};
}
async createOrUpdateRecordSet(zoneName: string, recordType: string, relativeRecordSetName: string, value: string) {
const client = await this.getDnsManagementClient();
this.ctx.logger.info(`创建/更新记录集: ${relativeRecordSetName}.${zoneName}, 类型: ${recordType}, 值: ${value}`);
// 获取现有记录集
let existingRecordSet: any = null;
try {
existingRecordSet = await client.recordSets.get(
this.resourceGroupName,
zoneName,
relativeRecordSetName,
recordType as any
);
} catch (e) {
// 记录集不存在,这是正常的
}
let txtRecords: any[] = [];
if (existingRecordSet && existingRecordSet.txtRecords) {
txtRecords = [...existingRecordSet.txtRecords];
// 检查是否已存在相同的记录,避免重复
const exists = txtRecords.some(r => r.value && r.value.includes(value));
if (exists) {
this.ctx.logger.info(`记录值已存在,无需重复添加: ${value}`);
return existingRecordSet;
}
}
// 添加新记录
txtRecords.push({
value: [value]
});
const recordSet = await client.recordSets.createOrUpdate(
this.resourceGroupName,
zoneName,
relativeRecordSetName,
recordType as any,
{
ttl: 60,
txtRecords: txtRecords
}
);
await utils.sleep(3000);
return recordSet;
}
async deleteRecordSet(zoneName: string, recordType: string, relativeRecordSetName: string, value: string) {
const client = await this.getDnsManagementClient();
this.ctx.logger.info(`删除记录值: ${relativeRecordSetName}.${zoneName}, 类型: ${recordType}, 值: ${value}`);
try {
// 获取现有记录集
const existingRecordSet = await client.recordSets.get(
this.resourceGroupName,
zoneName,
relativeRecordSetName,
recordType as any
);
if (!existingRecordSet.txtRecords || existingRecordSet.txtRecords.length === 0) {
this.ctx.logger.info('记录集不存在或已为空,无需删除');
return;
}
// 过滤掉我们要删除的那个值
const filteredTxtRecords = existingRecordSet.txtRecords.filter((r: any) => {
return !(r.value && r.value.includes(value));
});
if (filteredTxtRecords.length === existingRecordSet.txtRecords.length) {
this.ctx.logger.info(`未找到要删除的记录值: ${value}`);
return;
}
if (filteredTxtRecords.length === 0) {
// 如果没有记录了,就删除整个记录集
this.ctx.logger.info('删除空记录集');
await client.recordSets.delete(
this.resourceGroupName,
zoneName,
relativeRecordSetName,
recordType as any
);
} else {
// 还有其他记录,只更新记录集,移除我们的值
this.ctx.logger.info(`更新记录集,移除指定值,剩余 ${filteredTxtRecords.length} 条记录`);
await client.recordSets.createOrUpdate(
this.resourceGroupName,
zoneName,
relativeRecordSetName,
recordType as any,
{
ttl: existingRecordSet.ttl,
txtRecords: filteredTxtRecords
}
);
}
} catch (e: any) {
this.ctx.logger.warn(`删除记录时出错: ${e.message}`);
}
}
}
new AzureAccess();
@@ -0,0 +1,63 @@
import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { AzureAccess } from './access.js';
import { PageRes, PageSearch } from '@certd/pipeline';
@IsDnsProvider({
name: 'azure-dns',
title: 'Azure DNS',
desc: 'Azure DNS 解析提供商',
accessType: 'azure',
icon: 'simple-icons:microsoftazure',
order: 1,
})
export class AzureDnsProvider extends AbstractDnsProvider {
access: AzureAccess;
async onInstance() {
this.access = this.ctx.access as AzureAccess;
}
async createRecord(options: CreateRecordOptions): Promise<any> {
const { fullRecord, value, type, domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
const zone = await this.access.getZoneId(domain);
this.logger.info(`获取到 DNS 区域: ${zone.name}, ID: ${zone.id}`);
const relativeRecordSetName = fullRecord.replace(`.${zone.name}`, '').replace(`.${zone.name.replace(/\.$/, '')}`, '');
await this.access.createOrUpdateRecordSet(zone.name, type, relativeRecordSetName, value);
return {
zoneId: zone.id,
zoneName: zone.name,
recordType: type,
relativeRecordSetName,
value: value,
};
}
async removeRecord(options: RemoveRecordOptions<any>): Promise<void> {
const { fullRecord, value: reqValue, type } = options.recordReq;
const record = options.recordRes;
if (!record) {
this.logger.warn('记录信息为空,不执行删除');
return;
}
try {
const value = record.value || reqValue;
await this.access.deleteRecordSet(record.zoneName, record.recordType, record.relativeRecordSetName, value);
this.logger.info(`删除域名解析成功:${fullRecord} ${value} ${type}`);
} catch (e: any) {
this.logger.warn(`删除域名解析失败:${e.message}`, );
}
}
async getDomainListPage(req: PageSearch): Promise<PageRes<DomainRecord>> {
return await this.access.listZonesPage(req);
}
}
new AzureDnsProvider();
@@ -0,0 +1,2 @@
export * from './access.js';
export * from './azure-dns-provider.js';