Merge branch 'v2-dev' into v2-dev-buy

This commit is contained in:
xiaojunnuo
2025-08-29 16:54:11 +08:00
78 changed files with 1559 additions and 225 deletions
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
### Bug Fixes
* 更新我爱云CDN域名地址,和部分目录结构 [@tyjsjxh](https://github.com/tyjsjxh) ([#514](https://github.com/certd/certd/issues/514)) ([78e7a81](https://github.com/certd/certd/commit/78e7a81638c2ee779f0ab6c3ba7e5c6f6e064151))
* 修复proxmox某些情况下执行卡住的bug ([ebd6917](https://github.com/certd/certd/commit/ebd6917a1d40ae4d94555c32b7e3c093d0599b94))
### Performance Improvements
* 部署到k8s支持自动创建secret ([c09c962](https://github.com/certd/certd/commit/c09c962cb676ca261610aa9f3e5105c9dae43f43))
* 短信验证码支持腾讯云 ([9108459](https://github.com/certd/certd/commit/9108459ae42bcd95a59acba164a64e82e5f2cfe6))
* 商业版支持自定义插件的参数配置 ([17f23f3](https://github.com/certd/certd/commit/17f23f37516af925d5049291d67d41e4271f81f8))
* 腾讯云插件支持国际版 ([58e82d5](https://github.com/certd/certd/commit/58e82d5dbd4ebf089ef239578ef9b68454d17b30))
* 腾讯云EO插件支持自动获取zoneid和域名列表 ([70fcdc9](https://github.com/certd/certd/commit/70fcdc9ebbfb7c883c0c8a2138f61a0776a9491b))
* 支持部署到阿里云云原生API网关、AI网关 ([2ca20be](https://github.com/certd/certd/commit/2ca20be197720201fceabcce9d927f4dbc1cc872))
* 支持部署到华为云obs ([9feb9d0](https://github.com/certd/certd/commit/9feb9d04b3c56ec95c06fcf4fd071eb0e88ffc6f))
* 支持部署到dokploy ([7dbdeae](https://github.com/certd/certd/commit/7dbdeaebe0bfee7521a863fe5e6b4a712aec5876))
* 支持删除宝塔证书夹中的过期证书 ([3575113](https://github.com/certd/certd/commit/3575113655be751d19f88c64491e98a89042d6a2))
* 支持p7b证书格式 ([d9f4a57](https://github.com/certd/certd/commit/d9f4a5793d68a017a5d80ad5385cbda603c4e165))
* lecdnv2支持api token ([e448934](https://github.com/certd/certd/commit/e4489343fee7754be07bcfc3323969dc3a30e90c))
* openapi返回证书时挑选匹配范围最小的那一个;增加format参数,增加返回值p7b格式,增加detail返回 ([2085bcc](https://github.com/certd/certd/commit/2085bcceb61c3723c9bdfec4c4cc0917631ff5e5))
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
### Bug Fixes
-2
View File
@@ -9,8 +9,6 @@
```
```shell
npm run heap
```
+15 -15
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.36.17",
"version": "1.36.18",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -42,20 +42,20 @@
"@aws-sdk/client-cloudfront": "^3.699.0",
"@aws-sdk/client-iam": "^3.699.0",
"@aws-sdk/client-s3": "^3.705.0",
"@certd/acme-client": "^1.36.17",
"@certd/basic": "^1.36.17",
"@certd/commercial-core": "^1.36.17",
"@certd/cv4pve-api-javascript": "^8.4.1",
"@certd/jdcloud": "^1.36.17",
"@certd/lib-huawei": "^1.36.17",
"@certd/lib-k8s": "^1.36.17",
"@certd/lib-server": "^1.36.17",
"@certd/midway-flyway-js": "^1.36.17",
"@certd/pipeline": "^1.36.17",
"@certd/plugin-cert": "^1.36.17",
"@certd/plugin-lib": "^1.36.17",
"@certd/plugin-plus": "^1.36.17",
"@certd/plus-core": "^1.36.17",
"@certd/acme-client": "^1.36.18",
"@certd/basic": "^1.36.18",
"@certd/commercial-core": "^1.36.18",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.36.18",
"@certd/lib-huawei": "^1.36.18",
"@certd/lib-k8s": "^1.36.18",
"@certd/lib-server": "^1.36.18",
"@certd/midway-flyway-js": "^1.36.18",
"@certd/pipeline": "^1.36.18",
"@certd/plugin-cert": "^1.36.18",
"@certd/plugin-lib": "^1.36.18",
"@certd/plugin-plus": "^1.36.18",
"@certd/plus-core": "^1.36.18",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.120",
"@huaweicloud/huaweicloud-sdk-core": "^3.1.120",
"@koa/cors": "^5.0.0",
@@ -0,0 +1,21 @@
import {Controller, Get, Provide} from '@midwayjs/core';
import {BaseController, Constants} from '@certd/lib-server';
/**
*/
@Provide()
@Controller('/health')
export class HealthController extends BaseController {
@Get('/liveliness', { summary: Constants.per.guest })
async liveliness(): Promise<any> {
return this.ok('ok')
}
@Get('/readiness', { summary: Constants.per.guest })
async readiness(): Promise<any> {
return this.ok('ok')
}
}
@@ -10,6 +10,7 @@ export type CertGetReq = {
domains?: string;
certId: number;
autoApply?:boolean;
format?:string; //默认是所有,pem,der,p12,pfx,jks,one,p7b
};
/**
@@ -38,6 +39,7 @@ export class OpenCertController extends BaseOpenController {
domains: req.domains,
certId: req.certId,
autoApply: req.autoApply??false,
format: req.format
});
return this.ok(res);
}
@@ -2,7 +2,11 @@ import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/c
import { merge } from 'lodash-es';
import { CrudController } from '@certd/lib-server';
import { PluginImportReq, PluginService } from "../../../modules/plugin/service/plugin-service.js";
import { CommPluginConfig, PluginConfigService } from '../../../modules/plugin/service/plugin-config-service.js';
import {
CommPluginConfig,
PluginConfig,
PluginConfigService
} from '../../../modules/plugin/service/plugin-config-service.js';
/**
* 插件
*/
@@ -79,7 +83,11 @@ export class PluginController extends CrudController<PluginService> {
const res = await this.pluginConfigService.saveCommPluginConfig(body);
return this.ok(res);
}
@Post('/saveSetting', { summary: 'sys:settings:edit' })
async saveSetting(@Body(ALL) body: PluginConfig) {
const res = await this.pluginConfigService.savePluginConfig(body);
return this.ok(res);
}
@Post('/import', { summary: 'sys:settings:edit' })
async import(@Body(ALL) body: PluginImportReq) {
@@ -164,7 +164,8 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
@Post('/getSmsTypeDefine', { summary: 'sys:settings:view' })
async getSmsTypeDefine(@Body('type') type: string) {
return this.ok(SmsServiceFactory.getDefine(type));
const define =await SmsServiceFactory.getDefine(type);
return this.ok(define);
}
@@ -83,7 +83,7 @@ export class CodeService {
}
const smsType = sysSettings.sms.type;
const smsConfig = sysSettings.sms.config;
const sender: ISmsService = SmsServiceFactory.createSmsService(smsType);
const sender: ISmsService = await SmsServiceFactory.createSmsService(smsType);
const accessGetter = new AccessSysGetter(this.accessService);
sender.setCtx({
accessService: accessGetter,
@@ -1,25 +1,28 @@
import { AliyunSmsService } from './aliyun-sms.js';
import { YfySmsService } from './yfy-sms.js';
export class SmsServiceFactory {
static createSmsService(type: string) {
const cls = this.GetClassByType(type);
static async createSmsService(type: string) {
const cls = await this.GetClassByType(type);
return new cls();
}
static GetClassByType(type: string) {
static async GetClassByType(type: string) {
switch (type) {
case 'aliyun':
const {AliyunSmsService} = await import("./aliyun-sms.js")
return AliyunSmsService;
case 'yfysms':
const {YfySmsService} = await import("./yfy-sms.js")
return YfySmsService;
case 'tencent':
const {TencentSmsService} = await import("./tencent-sms.js")
return TencentSmsService;
default:
throw new Error('不支持的短信服务类型');
}
}
static getDefine(type: string) {
const cls = this.GetClassByType(type);
static async getDefine(type: string) {
const cls = await this.GetClassByType(type);
return cls.getDefine();
}
}
@@ -0,0 +1,124 @@
import {ISmsService, PluginInputs, SmsPluginCtx} from './api.js';
import {TencentAccess} from "@certd/plugin-lib";
export type TencentSmsConfig = {
accessId: string;
signName: string;
codeTemplateId: string;
appId: string;
region: string;
};
export class TencentSmsService implements ISmsService {
static getDefine() {
return {
name: 'tencent',
desc: '腾讯云短信服务',
input: {
accessId: {
title: '腾讯云授权',
component: {
name: 'access-selector',
type: 'tencent',
},
required: true,
},
region: {
title: '区域',
value:"ap-beijing",
component: {
name: 'a-select',
vModel: 'value',
options:[
{value:"ap-beijing",label:"华北地区(北京)"},
{value:"ap-guangzhou",label:"华南地区(广州)"},
{value:"ap-nanjing",label:"华东地区(南京)"},
]
},
helper:"随便选一个",
required: true,
},
signName: {
title: '签名',
component: {
name: 'a-input',
vModel: 'value',
},
required: true,
},
appId: {
title: '应用ID',
component: {
name: 'a-input',
vModel: 'value',
},
required: true,
},
codeTemplateId: {
title: '验证码模板Id',
component: {
name: 'a-input',
vModel: 'value',
},
required: true,
},
} as PluginInputs<TencentSmsConfig>,
};
}
ctx: SmsPluginCtx<TencentSmsConfig>;
setCtx(ctx: any) {
this.ctx = ctx;
}
async getClient() {
const sdk = await import('tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js');
const client = sdk.v20210111.Client;
const access = await this.ctx.accessService.getById<TencentAccess>(this.ctx.config.accessId);
// const region = this.region;
const clientConfig = {
credential: {
secretId: access.secretId,
secretKey: access.secretKey,
},
region: this.ctx.config.region,
profile: {
httpProfile: {
endpoint: `sms.${access.intlDomain()}tencentcloudapi.com`,
},
},
};
return new client(clientConfig);
}
async sendSmsCode(opts: { mobile: string; code: string; phoneCode: string }) {
const { mobile, code, phoneCode } = opts;
const client = await this.getClient();
const smsConfig = this.ctx.config;
const params = {
"PhoneNumberSet": [
`+${phoneCode}${mobile}`
],
"SmsSdkAppId": smsConfig.appId,
"TemplateId": smsConfig.codeTemplateId,
"SignName": smsConfig.signName,
"TemplateParamSet": [
code
]
};
const ret = await client.SendSms(params);
this.checkRet(ret);
}
checkRet(ret: any) {
if (!ret || ret.Error) {
throw new Error('执行失败:' + ret.Error.Code + ',' + ret.Error.Message);
}
}
}
@@ -27,7 +27,7 @@ export class CertInfoFacade {
@Inject()
userSettingsService : UserSettingsService
async getCertInfo(req: { domains?: string; certId?: number; userId: number,autoApply?:boolean }) {
async getCertInfo(req: { domains?: string; certId?: number; userId: number,autoApply?:boolean,format?:string }) {
const { domains, certId, userId } = req;
if (certId) {
return await this.certInfoService.getCertInfoById({ id: certId, userId });
@@ -41,7 +41,7 @@ export class CertInfoFacade {
const domainArr = domains.split(',');
const matchedList = await this.certInfoService.getMatchCertList({domains:domainArr,userId})
let matched: CertInfoEntity = null
if (matchedList.length === 0 ) {
if(req.autoApply === true){
//自动申请,先创建自动申请流水线
@@ -54,13 +54,7 @@ export class CertInfoFacade {
});
}
}
matched = null;
for (const item of matchedList) {
if (item.expiresTime>0 && item.expiresTime > new Date().getTime()) {
matched = item;
break
}
}
let matched = this.getMinixMatched(matchedList);
if (!matched) {
if(req.autoApply === true){
//如果没有找到有效期内的证书,则自动触发一次申请
@@ -75,7 +69,38 @@ export class CertInfoFacade {
}
}
return await this.certInfoService.getCertInfoById({ id: matched.id, userId: userId });
return await this.certInfoService.getCertInfoById({ id: matched.id, userId: userId,format:req.format });
}
public getMinixMatched(matchedList: CertInfoEntity[]) {
let matched: CertInfoEntity = null;
for (const item of matchedList) {
if (item.expiresTime > 0 && item.expiresTime > new Date().getTime()) {
if (matched) {
//如果前面已经有match的值,判断范围是否比上一个小
const currentStars = `-${item.domains}`.split("*");
const matchedStars = `-${matched.domains}`.split("*");
const currentLength = item.domains.split(",");
const matchedLength = matched.domains.split(",");
if (currentStars.length < matchedStars.length) {
//如果*的数量比上一个少,则替换为当前
matched = item;
} else if (currentStars.length == matchedStars.length) {
//如果*的数量相同,则比较域名数量
if (currentLength.length < matchedLength.length) {
matched = item;
}
}
} else {
matched = item;
}
}
}
return matched;
}
async createAutoPipeline(req:{domains:string[],userId:number}){
@@ -113,7 +113,7 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
});
}
async getCertInfoById(req: { id: number; userId: number }) {
async getCertInfoById(req: { id: number; userId: number,format?:string }) {
const entity = await this.info(req.id);
if (!entity || entity.userId !== req.userId) {
throw new CodeException(Constants.res.openCertNotFound);
@@ -124,7 +124,14 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
}
const certInfo = JSON.parse(entity.certInfo) as CertInfo;
const certReader = new CertReader(certInfo);
return certReader.toCertInfo();
return {
...certReader.toCertInfo(req.format),
detail: {
id: entity.id,
domains: entity.domains.split(','),
notAfter: certReader.expires,
},
};
}
async updateCertByPipelineId(pipelineId: number, cert: CertInfo,file?:string,fromType = 'pipeline') {
@@ -3,9 +3,10 @@ import { PluginService } from './plugin-service.js';
export type PluginConfig = {
name: string;
disabled: boolean;
disabled?: boolean;
sysSetting: {
input?: Record<string, any>;
metadata?: Record<string, any>;
};
};
@@ -37,10 +38,12 @@ export class PluginConfigService {
}
async saveCommPluginConfig(config: CommPluginConfig) {
await this.savePluginConfig('CertApply', config.CertApply);
config.CertApply.name = 'CertApply';
await this.savePluginConfig(config.CertApply);
}
async savePluginConfig(name: string, config: PluginConfig) {
async savePluginConfig( config: PluginConfig) {
const name = config.name;
const sysSetting = config?.sysSetting;
if (!sysSetting) {
throw new Error(`${name}.sysSetting is required`);
@@ -57,7 +60,14 @@ export class PluginConfigService {
author: "certd",
});
} else {
await this.pluginService.getRepository().update({ name }, { sysSetting: JSON.stringify(sysSetting) });
let setting = JSON.parse(pluginEntity.sysSetting || "{}");
if (sysSetting.metadata) {
setting.metadata = sysSetting.metadata;
}
if (sysSetting.input) {
setting.input = sysSetting.input;
}
await this.pluginService.getRepository().update({ name }, { sysSetting: JSON.stringify(setting) });
}
}
@@ -1,16 +1,16 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { BaseService, PageReq } from "@certd/lib-server";
import { PluginEntity } from "../entity/plugin.js";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { isComm } from "@certd/plus-core";
import { BuiltInPluginService } from "../../pipeline/service/builtin-plugin-service.js";
import { merge } from "lodash-es";
import { accessRegistry, notificationRegistry, pluginRegistry } from "@certd/pipeline";
import { dnsProviderRegistry } from "@certd/plugin-cert";
import { logger } from "@certd/basic";
import {Inject, Provide, Scope, ScopeEnum} from "@midwayjs/core";
import {BaseService, PageReq} from "@certd/lib-server";
import {PluginEntity} from "../entity/plugin.js";
import {InjectEntityModel} from "@midwayjs/typeorm";
import {IsNull, Not, Repository} from "typeorm";
import {isComm} from "@certd/plus-core";
import {BuiltInPluginService} from "../../pipeline/service/builtin-plugin-service.js";
import {merge} from "lodash-es";
import {accessRegistry, notificationRegistry, pluginRegistry} from "@certd/pipeline";
import {dnsProviderRegistry} from "@certd/plugin-cert";
import {logger} from "@certd/basic";
import yaml from "js-yaml";
import { getDefaultAccessPlugin, getDefaultDeployPlugin, getDefaultDnsPlugin } from "./default-plugin.js";
import {getDefaultAccessPlugin, getDefaultDeployPlugin, getDefaultDnsPlugin} from "./default-plugin.js";
import fs from "fs";
import path from "path";
@@ -57,9 +57,9 @@ export class PluginService extends BaseService<PluginEntity> {
};
}
async getEnabledBuildInGroup(isSimple = false) {
async getEnabledBuildInGroup(opts?:{isSimple?:boolean,withSetting?:boolean}) {
const groups = this.builtInPluginService.getGroups();
if (isSimple) {
if (opts?.isSimple) {
for (const key in groups) {
const group = groups[key];
group.plugins.forEach(item => {
@@ -72,9 +72,43 @@ export class PluginService extends BaseService<PluginEntity> {
if (!isComm()) {
return groups;
}
// 初始化设置
const settingPlugins = await this.repository.find({
select:{
id:true,
name:true,
sysSetting:true
},
where: {
sysSetting : Not(IsNull())
}
})
//合并插件配置
const pluginSettingMap:any = {}
for (const item of settingPlugins) {
if (!item.sysSetting) {
continue;
}
pluginSettingMap[item.name] = JSON.parse(item.sysSetting);
}
for (const key in groups) {
const group = groups[key];
if (!group.plugins) {
continue;
}
for (const item of group.plugins) {
const pluginSetting = pluginSettingMap[item.name];
if (pluginSetting){
item.sysSetting = pluginSetting
}
}
}
//排除禁用的
const list = await this.list({
query: {
type: "builtIn",
disabled: true
}
});
@@ -33,3 +33,4 @@ export * from './plugin-wangsu/index.js'
export * from './plugin-admin/index.js'
export * from './plugin-ksyun/index.js'
export * from './plugin-apisix/index.js'
export * from './plugin-dokploy/index.js'
@@ -0,0 +1,272 @@
import {AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput} from '@certd/pipeline';
import {
AliyunAccess,
AliyunSslClient,
createCertDomainGetterInputDefine,
createRemoteSelectInputDefine
} from "@certd/plugin-lib";
import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
import {optionsUtils} from "@certd/basic/dist/utils/util.options.js";
@IsTaskPlugin({
name: 'DeployCertToAliyunApig',
title: '阿里云-部署至云原生API网关/AI网关',
icon: 'svg:icon-aliyun',
group: pluginGroups.aliyun.key,
desc: '自动部署域名证书至云原生API网关、AI网关',
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
export class DeployCertToAliyunApig extends AbstractTaskPlugin {
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'output-selector',
from: [...CertApplyPluginNames, 'uploadCertToAliyun'],
},
required: true,
})
cert!: CertInfo | string;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
@TaskInput({
title: 'Access授权',
helper: '阿里云授权',
component: {
name: 'access-selector',
type: 'aliyun',
},
required: true,
})
accessId!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: '区域',
helper: '请选择区域',
action: DeployCertToAliyunApig.prototype.onGetRegionList.name,
watches: ['certDomains', 'accessId'],
required: true,
component:{
name:"remote-auto-complete"
}
})
)
regionEndpoint!: string;
@TaskInput({
title: "网关类型",
component: {
name: "a-select",
vModel:"value",
options:[
{value:"AI",label:"AI"},
{value:"API",label:"API"},
]
},
required: true //必填
})
gatewayType!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: '绑定域名',
helper: '请选择域名',
action: DeployCertToAliyunApig.prototype.onGetDomainList.name,
watches: ['region', 'accessId','gatewayType'],
required: true,
})
)
domainList!: string[];
@TaskInput({
title: "强制HTTPS",
component: {
name: "a-select",
vModel:"value",
options:[
{value:true,label:"强制HTTPS"},
{value:false,label:"不强制HTTPS"},
]
},
required: true //必填
})
forceHttps!: boolean;
@TaskInput({
title: '证书服务接入点',
helper: '不会选就按默认',
value: 'cn-hangzhou',
component: {
name: 'a-select',
options: [
{ value: 'cn-hangzhou', label: '中国大陆' },
{ value: 'ap-southeast-1', label: '新加坡' },
],
},
required: true,
})
casRegion!: string;
async onInstance() {}
async execute(): Promise<void> {
this.logger.info('开始部署证书到云原生Api网关');
if(!this.domainList){
throw new Error('您还未选择域名');
}
const access = await this.getAccess<AliyunAccess>(this.accessId);
const client = access.getClient(this.regionEndpoint)
let certId: any = this.cert;
if (typeof this.cert === 'object') {
const sslClient = new AliyunSslClient({
access,
logger: this.logger,
region: this.casRegion,
});
certId = await sslClient.uploadCert({
name: this.buildCertName(CertReader.getMainDomain(this.cert.crt)),
cert: this.cert,
});
}
const certIdentify = `${certId}-${this.casRegion}`
for (const domainId of this.domainList ) {
this.logger.info(`[${domainId}]开始部署`)
await this.updateCert(client, domainId,certIdentify);
this.logger.info(`[${domainId}]部署成功`)
}
this.logger.info('部署完成');
}
async updateCert(client: any, domainId: string,certIdentify:string) {
const domainInfoRes = await client.doRequest({
action: "GetDomain",
version: "2024-03-27",
protocol: "HTTPS",
method: "GET",
authType: "AK",
style: "ROA",
pathname: `/v1/domains/${domainId}`,
});
const tlsCipherSuitesConfig = domainInfoRes.data?.tlsCipherSuitesConfig
const ret = await client.doRequest({
action: "UpdateDomain",
version: "2024-03-27",
method: "PUT",
style: "ROA",
pathname: `/v1/domains/${domainId}`,
data:{
body:{
certIdentifier: certIdentify,
protocol: "HTTPS",
forceHttps:this.forceHttps,
tlsCipherSuitesConfig
}
}
})
this.logger.info(`设置${domainId}证书成功:`, ret.requestId);
}
async onGetDomainList(data: any) {
if (!this.accessId) {
throw new Error('请选择Access授权');
}
if (!this.regionEndpoint) {
throw new Error('请选择区域');
}
if (!this.gatewayType) {
throw new Error('请选择网关类型');
}
const access = await this.getAccess<AliyunAccess>(this.accessId);
const client = access.getClient(this.regionEndpoint)
const res =await client.doRequest({
action: "ListDomains",
version: "2024-03-27",
method: "GET",
style: "ROA",
pathname: `/v1/domains`,
data:{
query:{
pageSize: 100,
gatewayType: this.gatewayType ,
}
}
})
const list = res?.data?.items;
if (!list || list.length === 0) {
return []
}
const options = list.map((item: any) => {
return {
value: item.domainId,
label: `${item.name}<${item.domainId}>`,
domain: item.name,
};
});
return optionsUtils.buildGroupOptions(options, this.certDomains);
}
async onGetRegionList(data: any) {
const list = [
{value:"cn-qingdao",label:"华北1(青岛)",endpoint:"apig.cn-qingdao.aliyuncs.com"},
{value:"cn-beijing",label:"华北2(北京)",endpoint:"apig.cn-beijing.aliyuncs.com"},
{value:"cn-zhangjiakou",label:"华北3(张家口)",endpoint:"apig.cn-zhangjiakou.aliyuncs.com"},
{value:"cn-wulanchabu",label:"华北6(乌兰察布)",endpoint:"apig.cn-wulanchabu.aliyuncs.com"},
{value:"cn-hangzhou",label:"华东1(杭州)",endpoint:"apig.cn-hangzhou.aliyuncs.com"},
{value:"cn-shanghai",label:"华东2(上海)",endpoint:"apig.cn-shanghai.aliyuncs.com"},
{value:"cn-shenzhen",label:"华南1(深圳)",endpoint:"apig.cn-shenzhen.aliyuncs.com"},
{value:"cn-heyuan",label:"华南2(河源)",endpoint:"apig.cn-heyuan.aliyuncs.com"},
{value:"cn-guangzhou",label:"华南3(广州)",endpoint:"apig.cn-guangzhou.aliyuncs.com"},
{value:"ap-southeast-2",label:"澳大利亚(悉尼)已关停",endpoint:"apig.ap-southeast-2.aliyuncs.com"},
{value:"ap-southeast-6",label:"菲律宾(马尼拉)",endpoint:"apig.ap-southeast-6.aliyuncs.com"},
{value:"ap-northeast-2",label:"韩国(首尔)",endpoint:"apig.ap-northeast-2.aliyuncs.com"},
{value:"ap-southeast-3",label:"马来西亚(吉隆坡)",endpoint:"apig.ap-southeast-3.aliyuncs.com"},
{value:"ap-northeast-1",label:"日本(东京)",endpoint:"apig.ap-northeast-1.aliyuncs.com"},
{value:"ap-southeast-7",label:"泰国(曼谷)",endpoint:"apig.ap-southeast-7.aliyuncs.com"},
{value:"cn-chengdu",label:"西南1(成都)",endpoint:"apig.cn-chengdu.aliyuncs.com"},
{value:"ap-southeast-1",label:"新加坡",endpoint:"apig.ap-southeast-1.aliyuncs.com"},
{value:"ap-southeast-5",label:"印度尼西亚(雅加达)",endpoint:"apig.ap-southeast-5.aliyuncs.com"},
{value:"cn-hongkong",label:"中国香港",endpoint:"apig.cn-hongkong.aliyuncs.com"},
{value:"eu-central-1",label:"德国(法兰克福)",endpoint:"apig.eu-central-1.aliyuncs.com"},
{value:"us-east-1",label:"美国(弗吉尼亚)",endpoint:"apig.us-east-1.aliyuncs.com"},
{value:"us-west-1",label:"美国(硅谷)",endpoint:"apig.us-west-1.aliyuncs.com"},
{value:"eu-west-1",label:"英国(伦敦)",endpoint:"apig.eu-west-1.aliyuncs.com"},
{value:"me-east-1",label:"阿联酋(迪拜)",endpoint:"apig.me-east-1.aliyuncs.com"},
{value:"me-central-1",label:"沙特(利雅得)",endpoint:"apig.me-central-1.aliyuncs.com"},
]
return list.map((item: any) => {
return {
value: item.endpoint,
label: item.label,
endpoint: item.endpoint,
regionId : item.value
};
})
}
}
new DeployCertToAliyunApig();
@@ -10,3 +10,4 @@ export * from './deploy-to-fc/index.js';
export * from './deploy-to-esa/index.js';
export * from './deploy-to-vod/index.js';
export * from './deploy-to-apigateway/index.js';
export * from './deploy-to-apig/index.js';
@@ -14,8 +14,9 @@ import { CertApplyPluginNames, CertReader } from "@certd/plugin-cert";
*/
const regionDict = [
{ value: 'cn-hangzhou', endpoint: 'cas.aliyuncs.com', label: 'cn-hangzhou-中国大陆' },
{ value: 'eu-central-1', endpoint: 'cas.eu-central-1.aliyuncs.com', label: 'eu-central-1-德国(法兰克福)' },
{ value: 'ap-southeast-1', endpoint: 'cas.ap-southeast-1.aliyuncs.com', label: 'ap-southeast-1-新加坡(国际版选这个)' },
{ value: 'private-', endpoint: '', disabled:true, label: '以下是私有证书区域' },
{ value: 'eu-central-1', endpoint: 'cas.eu-central-1.aliyuncs.com', label: 'eu-central-1-德国(法兰克福)' },
{ value: 'ap-southeast-3', endpoint: 'cas.ap-southeast-3.aliyuncs.com', label: 'ap-southeast-3-马来西亚(吉隆坡)' },
{ value: 'ap-southeast-5', endpoint: 'cas.ap-southeast-5.aliyuncs.com', label: 'ap-southeast-5-印度尼西亚(雅加达)' },
{ value: 'cn-hongkong', endpoint: 'cas.cn-hongkong.aliyuncs.com', label: 'cn-hongkong-中国香港' },
@@ -8,7 +8,7 @@ import {CertInfo, CertReader} from "@certd/plugin-cert";
name: "apisix",
title: "APISIX授权",
desc: "",
icon: "svg:icon-ksyun"
icon: "svg:icon-lucky"
})
export class ApisixAccess extends BaseAccess {
@@ -93,7 +93,7 @@ export class ApisixAccess extends BaseAccess {
headers,
baseURL: this.endpoint,
...req,
logRes: true,
logRes: false,
});
}
@@ -0,0 +1,107 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
import { HttpRequestConfig } from "@certd/basic";
import { CertInfo } from "@certd/plugin-cert";
/**
*/
@IsAccess({
name: "dokploy",
title: "Dokploy授权",
desc: "",
icon: "svg:icon-lucky"
})
export class DokployAccess extends BaseAccess {
@AccessInput({
title: "Dokploy地址",
component: {
placeholder: "http://192.168.11.11:5480",
},
required: true,
})
endpoint = '';
@AccessInput({
title: 'ApiKey',
component: {
placeholder: 'ApiKey',
},
// naAyXbZmxtsfrDfneOCeirbQNIICmBgfBiYXQwryPIUOdzPkXkfnaKjeAdbOQdwp
//tlyvdNzojaFkNfGScALLmyuFHkHcYWaxoYjiDzWFHcnZAWdjOquMSqBwHLvGDGZK
helper: "[settings-profile](https://app.dokploy.com/dashboard/settings/profile)中配置API Keys",
required: true,
encrypt: true,
})
apiKey = '';
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "TestRequest"
},
helper: "点击测试接口是否正常"
})
testRequest = true;
async onTestRequest() {
await this.getCertList();
return "ok"
}
async getCertList(){
const req = {
url :"/api/certificates.all",
method: "get",
}
return await this.doRequest(req);
}
async createCert(opts:{cert:CertInfo,serverId:string,name:string}){
const req = {
url :"/api/certificates.create",
method: "post",
data:{
// certificateId:opts.certificateId,
"name": opts.name,
"certificateData": opts.cert.crt,
"privateKey": opts.cert.key,
"serverId": opts.serverId,
autoRenew: false,
organizationId : ""
}
}
return await this.doRequest(req);
}
async removeCert (opts:{id:string}){
const req = {
url :"/api/certificates.remove",
method: "post",
data:{
certificateId:opts.id,
}
}
return await this.doRequest(req);
}
async doRequest(req: HttpRequestConfig){
const headers = {
"x-api-key": this.apiKey,
...req.headers
};
return await this.ctx.http.request({
headers,
baseURL: this.endpoint,
...req,
logRes: true,
});
}
}
new DokployAccess();
@@ -0,0 +1,2 @@
export * from "./plugins/index.js";
export * from "./access.js";
@@ -0,0 +1 @@
import "./plugin-refresh-cert.js"
@@ -0,0 +1,117 @@
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import {CertApplyPluginNames, CertInfo} from "@certd/plugin-cert";
import {createCertDomainGetterInputDefine, createRemoteSelectInputDefine} from "@certd/plugin-lib";
import {DokployAccess} from "../access.js";
@IsTaskPlugin({
//命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名
name: "DokployRefreshCert",
title: "Dokploy-更新证书",
desc: "自动更新Dokploy证书",
icon: "svg:icon-lucky",
//插件分组
group: pluginGroups.panel.key,
needPlus: true,
default: {
//默认值配置照抄即可
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed
}
}
})
//类名规范,跟上面插件名称(name)一致
export class DokployRefreshCert extends AbstractTaskPlugin {
//证书选择,此项必须要有
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "output-selector",
from: [...CertApplyPluginNames]
}
// required: true, // 必填
})
cert!: CertInfo;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
//授权选择框
@TaskInput({
title: "Dokploy授权",
component: {
name: "access-selector",
type: "dokploy" //固定授权类型
},
required: true //必填
})
accessId!: string;
//
@TaskInput(
createRemoteSelectInputDefine({
title: "证书名称",
helper: "要更新的证书名称,如果这里没有,请先给手动绑定一次证书",
action: DokployRefreshCert.prototype.onGetCertList.name,
pager: false,
search: false
})
)
certList!: string[];
//插件实例化时执行的方法
async onInstance() {
}
//插件执行方法
async execute(): Promise<void> {
const access = await this.getAccess<DokployAccess>(this.accessId);
// await access.createCert({cert:this.cert})
const certList = await access.getCertList();
for (const certId of this.certList) {
this.logger.info(`----------- 开始更新证书:${certId}`);
const [serverId,name] = certId.split("#");
const founds = certList.filter((item: any) => item.name === name);
if (founds){
for (const found of founds) {
await access.removeCert({id:found.certificateId})
}
}
await access.createCert({
name,
cert: this.cert,
serverId: serverId,
});
this.logger.info(`----------- 更新证书${certId}成功`);
}
this.logger.info("部署完成");
}
async onGetCertList(data: PageSearch = {}) {
const access = await this.getAccess<DokployAccess>(this.accessId);
const res = await access.getCertList()
const list = res
if (!list || list.length === 0) {
throw new Error("没有找到证书,你可以直接手动输入id,如果id不存在将自动创建");
}
const options = list.map((item: any) => {
return {
label: `${item.name}<${item.serverId}>`,
value: `${item.serverId}#${item.name}`,
domain: item.name
};
});
return options;
}
}
//实例化一下,注册插件
new DokployRefreshCert();
@@ -61,10 +61,12 @@ export class ProxmoxAccess extends BaseAccess {
@AccessInput({
title: '领域',
value: "pam",
component: {
placeholder: 'realm',
placeholder: 'pam、pve。默认值 pam',
},
required: true,
helper:"pam 或 pve。默认值 pam",
required: false,
encrypt: false,
})
realm = '';
@@ -71,8 +71,14 @@ export class ProxmoxUploadCert extends AbstractPlusTaskPlugin {
for (const node of this.nodes) {
this.logger.info(`开始上传证书到节点:${node}`);
const res = await client.nodes.get(node).certificates.custom.uploadCustomCert(cert.crt, true, cert.key, true);
this.logger.info(`上传结果:${JSON.stringify(res.response)}`);
try{
const res = await client.nodes.get(node).certificates.custom.uploadCustomCert(cert.crt, true, cert.key, true);
this.logger.info(`上传结果:${JSON.stringify(res.response)}`);
}catch (e) {
this.logger.error(`执行失败:${e.message},请检查节点名称是否正确`);
throw e
}
}
this.logger.info('部署成功');
@@ -34,20 +34,6 @@ export class DeployCertToTencentTKEIngressPlugin extends AbstractTaskPlugin {
})
ingressClass!: string;
/**
* AccessProvider的key,或者一个包含access的具体的对象
*/
@TaskInput({
title: "Access授权",
helper: "access授权",
component: {
name: "access-selector",
type: "tencent"
},
required: true
})
accessId!: string;
@TaskInput({
title: "腾讯云证书id",
helper: "请选择“上传证书到腾讯云”前置任务的输出",
@@ -66,6 +52,7 @@ export class DeployCertToTencentTKEIngressPlugin extends AbstractTaskPlugin {
})
tencentCertId!: string;
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
@@ -85,6 +72,24 @@ export class DeployCertToTencentTKEIngressPlugin extends AbstractTaskPlugin {
cert!: any;
/**
* AccessProvider的key,或者一个包含access的具体的对象
*/
@TaskInput({
title: "Access授权",
helper: "access授权",
component: {
name: "access-selector",
type: "tencent"
},
required: true
})
accessId!: string;
@TaskInput({ title: "大区", value: "ap-guangzhou", required: true })
region!: string;
@@ -147,6 +152,17 @@ export class DeployCertToTencentTKEIngressPlugin extends AbstractTaskPlugin {
})
skipTLSVerify!:boolean
@TaskInput({
title: "Secret自动创建",
helper: "如果Secret不存在,则创建",
value: false,
component: {
name: "a-switch",
vModel: "checked",
},
})
createOnNotFound: boolean;
// @TaskInput({ title: "集群内网ip", helper: "如果开启了外网的话,无需设置" })
// clusterIp!: string;
@@ -288,7 +304,7 @@ export class DeployCertToTencentTKEIngressPlugin extends AbstractTaskPlugin {
secretNames = [secretName];
}
for (const secret of secretNames) {
await k8sClient.patchSecret({ namespace, secretName: secret, body });
await k8sClient.patchSecret({ namespace, secretName: secret, body , createOnNotFound: this.createOnNotFound});
this.logger.info(`CertSecret已更新:${secret}`);
}
}