diff --git a/packages/core/pipeline/src/plugin/group.ts b/packages/core/pipeline/src/plugin/group.ts index 949f47ec0..7e5148db4 100644 --- a/packages/core/pipeline/src/plugin/group.ts +++ b/packages/core/pipeline/src/plugin/group.ts @@ -21,8 +21,9 @@ export const pluginGroups = { huawei: new PluginGroup("huawei", "华为云", 3), tencent: new PluginGroup("tencent", "腾讯云", 4), qiniu: new PluginGroup("qiniu", "七牛云", 5), - host: new PluginGroup("host", "主机", 6), - cdn: new PluginGroup("cdn", "CDN", 7), - panel: new PluginGroup("panel", "面板", 8), - other: new PluginGroup("other", "其他", 9), + aws: new PluginGroup("aws", "亚马逊云", 6), + host: new PluginGroup("host", "主机", 7), + cdn: new PluginGroup("cdn", "CDN", 8), + panel: new PluginGroup("panel", "面板", 9), + other: new PluginGroup("other", "其他", 10), }; diff --git a/packages/ui/certd-server/package.json b/packages/ui/certd-server/package.json index df9bdeb65..474e274f3 100644 --- a/packages/ui/certd-server/package.json +++ b/packages/ui/certd-server/package.json @@ -31,6 +31,9 @@ }, "dependencies": { "@alicloud/pop-core": "^1.7.10", + "@aws-sdk/client-acm": "^3.699.0", + "@aws-sdk/client-cloudfront": "^3.699.0", + "@aws-sdk/client-s3": "^3.705.0", "@certd/acme-client": "^1.28.2", "@certd/basic": "^1.28.2", "@certd/commercial-core": "^1.28.2", diff --git a/packages/ui/certd-server/src/plugins/index.ts b/packages/ui/certd-server/src/plugins/index.ts index a2eda0cd4..677708c59 100644 --- a/packages/ui/certd-server/src/plugins/index.ts +++ b/packages/ui/certd-server/src/plugins/index.ts @@ -13,3 +13,4 @@ export * from './plugin-woai/index.js'; export * from './plugin-cachefly/index.js'; export * from './plugin-gcore/index.js'; export * from './plugin-qnap/index.js'; +export * from './plugin-aws/index.js'; diff --git a/packages/ui/certd-server/src/plugins/plugin-aws/access.ts b/packages/ui/certd-server/src/plugins/plugin-aws/access.ts new file mode 100644 index 000000000..202175a1c --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-aws/access.ts @@ -0,0 +1,66 @@ +import { AccessInput, BaseAccess, IsAccess } from '@certd/pipeline'; + +export const AwsRegions = [ + { label: 'us-east-1', value: 'us-east-1' }, + { label: 'us-east-2', value: 'us-east-2' }, + { label: 'us-west-1', value: 'us-west-1' }, + { label: 'us-west-2', value: 'us-west-2' }, + { label: 'af-south-1', value: 'af-south-1' }, + { label: 'ap-east-1', value: 'ap-east-1' }, + { label: 'ap-northeast-1', value: 'ap-northeast-1' }, + { label: 'ap-northeast-2', value: 'ap-northeast-2' }, + { label: 'ap-northeast-3', value: 'ap-northeast-3' }, + { label: 'ap-south-1', value: 'ap-south-1' }, + { label: 'ap-south-2', value: 'ap-south-2' }, + { label: 'ap-southeast-1', value: 'ap-southeast-1' }, + { label: 'ap-southeast-2', value: 'ap-southeast-2' }, + { label: 'ap-southeast-3', value: 'ap-southeast-3' }, + { label: 'ap-southeast-4', value: 'ap-southeast-4' }, + { label: 'ap-southeast-5', value: 'ap-southeast-5' }, + { label: 'ca-central-1', value: 'ca-central-1' }, + { label: 'ca-west-1', value: 'ca-west-1' }, + { label: 'eu-central-1', value: 'eu-central-1' }, + { label: 'eu-central-2', value: 'eu-central-2' }, + { label: 'eu-north-1', value: 'eu-north-1' }, + { label: 'eu-south-1', value: 'eu-south-1' }, + { label: 'eu-south-2', value: 'eu-south-2' }, + { label: 'eu-west-1', value: 'eu-west-1' }, + { label: 'eu-west-2', value: 'eu-west-2' }, + { label: 'eu-west-3', value: 'eu-west-3' }, + { label: 'il-central-1', value: 'il-central-1' }, + { label: 'me-central-1', value: 'me-central-1' }, + { label: 'me-south-1', value: 'me-south-1' }, + { label: 'sa-east-1', value: 'sa-east-1' }, +]; + +@IsAccess({ + name: 'aws', + title: '亚马逊云aws授权', + desc: '', + icon: 'ant-design:aws-outlined', +}) +export class AwsAccess extends BaseAccess { + @AccessInput({ + title: 'accessKeyId', + component: { + placeholder: 'accessKeyId', + }, + helper: + '右上角->安全凭证->访问密钥,[点击前往](https://us-east-1.console.aws.amazon.com/iam/home?region=ap-east-1#/security_credentials/access-key-wizard)', + required: true, + }) + accessKeyId = ''; + + @AccessInput({ + title: 'secretAccessKey', + component: { + placeholder: 'secretAccessKey', + }, + required: true, + encrypt: true, + helper: '请妥善保管您的安全访问密钥。您可以在AWS管理控制台的IAM中创建新的访问密钥。', + }) + secretAccessKey = ''; +} + +new AwsAccess(); diff --git a/packages/ui/certd-server/src/plugins/plugin-aws/index.ts b/packages/ui/certd-server/src/plugins/plugin-aws/index.ts new file mode 100644 index 000000000..fdad254fb --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-aws/index.ts @@ -0,0 +1,2 @@ +export * from './plugins/index.js'; +export * from './access.js'; diff --git a/packages/ui/certd-server/src/plugins/plugin-aws/libs/aws-acm-client.ts b/packages/ui/certd-server/src/plugins/plugin-aws/libs/aws-acm-client.ts new file mode 100644 index 000000000..a0cc26845 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-aws/libs/aws-acm-client.ts @@ -0,0 +1,40 @@ +// 导入所需的 SDK 模块 +import { AwsAccess } from '../access.js'; +import { CertInfo } from '@certd/plugin-cert'; + +type AwsAcmClientOptions = { access: AwsAccess; region: string }; + +export class AwsAcmClient { + options: AwsAcmClientOptions; + access: AwsAccess; + region: string; + constructor(options: AwsAcmClientOptions) { + this.options = options; + this.access = options.access; + this.region = options.region; + } + async importCertificate(certInfo: CertInfo) { + // 创建 ACM 客户端 + const { ACMClient, ImportCertificateCommand } = await import('@aws-sdk/client-acm'); + const acmClient = new ACMClient({ + region: this.region, // 替换为您的 AWS 区域 + credentials: { + accessKeyId: this.access.accessKeyId, // 从环境变量中读取 + secretAccessKey: this.access.secretAccessKey, + }, + }); + + const cert = certInfo.crt.split('-----END CERTIFICATE-----')[0] + '-----END CERTIFICATE-----'; + // 构建上传参数 + const data = await acmClient.send( + new ImportCertificateCommand({ + Certificate: Buffer.from(cert), + PrivateKey: Buffer.from(certInfo.key), + // CertificateChain: certificateChain, // 可选 + }) + ); + console.log('Upload successful:', data); + // 返回证书 ARN(Amazon Resource Name) + return data.CertificateArn; + } +} diff --git a/packages/ui/certd-server/src/plugins/plugin-aws/plugins/index.ts b/packages/ui/certd-server/src/plugins/plugin-aws/plugins/index.ts new file mode 100644 index 000000000..b2dfca5d3 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-aws/plugins/index.ts @@ -0,0 +1 @@ +export * from './plugin-deploy-to-cloudfront.js'; diff --git a/packages/ui/certd-server/src/plugins/plugin-aws/plugins/plugin-deploy-to-cloudfront.ts b/packages/ui/certd-server/src/plugins/plugin-aws/plugins/plugin-deploy-to-cloudfront.ts new file mode 100644 index 000000000..0136573c7 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-aws/plugins/plugin-deploy-to-cloudfront.ts @@ -0,0 +1,160 @@ +import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline'; +import { CertInfo } from '@certd/plugin-cert'; +import { AwsAccess, AwsRegions } from '../access.js'; +import { AwsAcmClient } from '../libs/aws-acm-client.js'; +import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib'; +import { optionsUtils } from '@certd/basic/dist/utils/util.options.js'; + +@IsTaskPlugin({ + name: 'AwsDeployToCloudFront', + title: '部署证书到 AWS CloudFront', + desc: '部署证书到 AWS CloudFront', + icon: 'clarity:plugin-line', + group: pluginGroups.aws.key, + default: { + strategy: { + runStrategy: RunStrategy.SkipWhenSucceed, + }, + }, +}) +export class AwsDeployToCloudFront extends AbstractTaskPlugin { + @TaskInput({ + title: '域名证书', + helper: '请选择前置任务输出的域名证书', + component: { + name: 'output-selector', + from: ['CertApply', 'CertApplyLego', 'AwsUploadToACM'], + }, + required: true, + }) + cert!: CertInfo | string; + + @TaskInput(createCertDomainGetterInputDefine({ props: { required: false } })) + certDomains!: string[]; + + @TaskInput({ + title: '区域', + helper: '证书上传区域', + component: { + name: 'a-auto-complete', + vModel: 'value', + options: AwsRegions, + }, + required: true, + }) + region!: string; + + @TaskInput({ + title: 'Access授权', + helper: 'aws的授权', + component: { + name: 'access-selector', + type: 'aws', + }, + required: true, + }) + accessId!: string; + + @TaskInput( + createRemoteSelectInputDefine({ + title: '分配ID', + helper: '请选择distributions id', + action: AwsDeployToCloudFront.prototype.onGetDistributions.name, + required: true, + }) + ) + distributionIds!: string[]; + + async onInstance() {} + + async execute(): Promise { + const access = await this.getAccess(this.accessId); + + let certId = this.cert as string; + if (typeof this.cert !== 'string') { + //先上传 + certId = await this.uploadToACM(access, this.cert); + } + //部署到CloudFront + + const { CloudFrontClient, UpdateDistributionCommand, GetDistributionConfigCommand } = await import('@aws-sdk/client-cloudfront'); + const cloudFrontClient = new CloudFrontClient({ + region: this.region, + credentials: { + accessKeyId: access.accessKeyId, + secretAccessKey: access.secretAccessKey, + }, + }); + + // update-distribution + for (const distributionId of this.distributionIds) { + // get-distribution-config + const getDistributionConfigCommand = new GetDistributionConfigCommand({ + Id: distributionId, + }); + + const configData = await cloudFrontClient.send(getDistributionConfigCommand); + + const updateDistributionCommand = new UpdateDistributionCommand({ + DistributionConfig: { + ...configData.DistributionConfig, + ViewerCertificate: { + ...configData.DistributionConfig.ViewerCertificate, + CloudFrontDefaultCertificate: false, + ACMCertificateArn: certId, + }, + }, + Id: distributionId, + IfMatch: configData.ETag, + }); + await cloudFrontClient.send(updateDistributionCommand); + this.logger.info(`部署${distributionId}完成:`); + } + this.logger.info('部署完成'); + } + + private async uploadToACM(access: AwsAccess, cert: CertInfo) { + const acmClient = new AwsAcmClient({ + access, + region: this.region, + }); + const awsCertARN = await acmClient.importCertificate(cert); + this.logger.info('证书上传成功,id=', awsCertARN); + return awsCertARN; + } + + //查找分配ID列表选项 + async onGetDistributions() { + if (!this.accessId) { + throw new Error('请选择Access授权'); + } + + const access = await this.getAccess(this.accessId); + const { CloudFrontClient, ListDistributionsCommand } = await import('@aws-sdk/client-cloudfront'); + const cloudFrontClient = new CloudFrontClient({ + region: this.region, + credentials: { + accessKeyId: access.accessKeyId, + secretAccessKey: access.secretAccessKey, + }, + }); + // list-distributions + const listDistributionsCommand = new ListDistributionsCommand({}); + const data = await cloudFrontClient.send(listDistributionsCommand); + const distributions = data.DistributionList?.Items; + if (!distributions || distributions.length === 0) { + throw new Error('找不到CloudFront分配ID,您可以手动输入'); + } + + const options = distributions.map((item: any) => { + return { + value: item.Id, + label: `${item.DomainName}<${item.Id}>`, + domain: item.DomainName, + }; + }); + return optionsUtils.buildGroupOptions(options, this.certDomains); + } +} + +new AwsDeployToCloudFront(); diff --git a/packages/ui/certd-server/src/plugins/plugin-aws/plugins/plugin-upload-to-acm.ts b/packages/ui/certd-server/src/plugins/plugin-aws/plugins/plugin-upload-to-acm.ts new file mode 100644 index 000000000..a0265db11 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-aws/plugins/plugin-upload-to-acm.ts @@ -0,0 +1,71 @@ +import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline'; +import { CertInfo } from '@certd/plugin-cert'; +import { AwsAccess, AwsRegions } from '../access.js'; +import { AwsAcmClient } from '../libs/aws-acm-client.js'; + +@IsTaskPlugin({ + name: 'AwsUploadToACM', + title: '上传证书 AWS ACM', + desc: '上传证书 AWS ACM', + icon: 'clarity:plugin-line', + group: pluginGroups.aws.key, + default: { + strategy: { + runStrategy: RunStrategy.SkipWhenSucceed, + }, + }, +}) +export class AwsUploadToACM extends AbstractTaskPlugin { + @TaskInput({ + title: '域名证书', + helper: '请选择前置任务输出的域名证书', + component: { + name: 'output-selector', + from: ['CertApply', 'CertApplyLego'], + }, + required: true, + }) + cert!: CertInfo; + + @TaskInput({ + title: 'Access授权', + helper: 'aws的授权', + component: { + name: 'access-selector', + type: 'aws', + }, + required: true, + }) + accessId!: string; + @TaskInput({ + title: '区域', + helper: '证书上传区域', + component: { + name: 'a-auto-complete', + vModel: 'value', + options: AwsRegions, + }, + required: true, + }) + region!: string; + + @TaskOutput({ + title: '证书ARN', + }) + awsCertARN = ''; + + async onInstance() {} + + async execute(): Promise { + const { cert, accessId, region } = this; + const access = await this.accessService.getById(accessId); + const acmClient = new AwsAcmClient({ + access, + region, + }); + this.awsCertARN = await acmClient.importCertificate(cert); + this.logger.info('证书上传成功,id=', this.awsCertARN); + } +} + +new AwsUploadToACM();