diff --git a/packages/ui/certd-server/package.json b/packages/ui/certd-server/package.json index f5e5b6d2..75d47179 100644 --- a/packages/ui/certd-server/package.json +++ b/packages/ui/certd-server/package.json @@ -39,6 +39,7 @@ "@alicloud/tea-typescript": "^1.8.0", "@alicloud/tea-util": "^1.4.10", "@aws-sdk/client-acm": "^3.699.0", + "@aws-sdk/client-iam": "^3.699.0", "@aws-sdk/client-cloudfront": "^3.699.0", "@aws-sdk/client-s3": "^3.705.0", "@certd/acme-client": "^1.34.10", diff --git a/packages/ui/certd-server/src/plugins/index.ts b/packages/ui/certd-server/src/plugins/index.ts index 7fa95664..00bdced3 100644 --- a/packages/ui/certd-server/src/plugins/index.ts +++ b/packages/ui/certd-server/src/plugins/index.ts @@ -14,6 +14,7 @@ export * from './plugin-cachefly/index.js'; export * from './plugin-gcore/index.js'; export * from './plugin-qnap/index.js'; export * from './plugin-aws/index.js'; +export * from './plugin-aws-cn/index.js'; export * from './plugin-dnsla/index.js'; export * from './plugin-upyun/index.js'; export * from './plugin-volcengine/index.js' diff --git a/packages/ui/certd-server/src/plugins/plugin-aws-cn/access.ts b/packages/ui/certd-server/src/plugins/plugin-aws-cn/access.ts new file mode 100644 index 00000000..5e0c0221 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-aws-cn/access.ts @@ -0,0 +1,38 @@ +import { AccessInput, BaseAccess, IsAccess } from '@certd/pipeline'; + +export const AwsCNRegions = [ + { label: 'cn-north-1', value: 'cn-north-1' }, + { label: 'cn-northwest-1', value: 'cn-northwest-1' }, +]; + +@IsAccess({ + name: 'aws-cn', + title: '亚马逊云科技(国区)授权', + desc: '', + icon: 'svg:icon-aws', +}) +export class AwsCNAccess extends BaseAccess { + @AccessInput({ + title: 'accessKeyId', + component: { + placeholder: 'accessKeyId', + }, + helper: + '右上角->安全凭证->访问密钥,[点击前往](https://cn-north-1.console.amazonaws.cn/iam/home?region=cn-north-1#/security_credentials/access-key-wizard#)', + required: true, + }) + accessKeyId = ''; + + @AccessInput({ + title: 'secretAccessKey', + component: { + placeholder: 'secretAccessKey', + }, + required: true, + encrypt: true, + helper: '请妥善保管您的安全访问密钥。您可以在AWS管理控制台的IAM中创建新的访问密钥。', + }) + secretAccessKey = ''; +} + +new AwsCNAccess(); diff --git a/packages/ui/certd-server/src/plugins/plugin-aws-cn/index.ts b/packages/ui/certd-server/src/plugins/plugin-aws-cn/index.ts new file mode 100644 index 00000000..fdad254f --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-aws-cn/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-cn/libs/aws-iam-client.ts b/packages/ui/certd-server/src/plugins/plugin-aws-cn/libs/aws-iam-client.ts new file mode 100644 index 00000000..492e8ca7 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-aws-cn/libs/aws-iam-client.ts @@ -0,0 +1,42 @@ +// 导入所需的 SDK 模块 +import { AwsCNAccess } from '../access.js'; +import { CertInfo } from '@certd/plugin-cert'; + +type AwsIAMClientOptions = { access: AwsCNAccess; region: string }; + +export class AwsIAMClient { + options: AwsIAMClientOptions; + access: AwsCNAccess; + region: string; + constructor(options: AwsIAMClientOptions) { + this.options = options; + this.access = options.access; + this.region = options.region; + } + async importCertificate(certInfo: CertInfo, certName: string) { + // 创建 IAM 客户端 + const { IAMClient, UploadServerCertificateCommand } = await import('@aws-sdk/client-iam'); + const iamClient = new IAMClient({ + region: this.region, // 替换为您的 AWS 区域 + credentials: { + accessKeyId: this.access.accessKeyId, // 从环境变量中读取 + secretAccessKey: this.access.secretAccessKey, + }, + }); + + const cert = certInfo.crt.split('-----END CERTIFICATE-----')[0] + '-----END CERTIFICATE-----'; + const chain = certInfo.crt.split('-----END CERTIFICATE-----\n')[1]; + // 构建上传参数 + const command = new UploadServerCertificateCommand({ + Path: '/cloudfront/', + ServerCertificateName: certName, + CertificateBody: cert, + PrivateKey: certInfo.key, + CertificateChain: chain + }) + const data = await iamClient.send(command); + console.log('Upload successful:', data); + // 返回证书 ID + return data.ServerCertificateMetadata.ServerCertificateId; + } +} diff --git a/packages/ui/certd-server/src/plugins/plugin-aws-cn/plugins/index.ts b/packages/ui/certd-server/src/plugins/plugin-aws-cn/plugins/index.ts new file mode 100644 index 00000000..b2dfca5d --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-aws-cn/plugins/index.ts @@ -0,0 +1 @@ +export * from './plugin-deploy-to-cloudfront.js'; diff --git a/packages/ui/certd-server/src/plugins/plugin-aws-cn/plugins/plugin-deploy-to-cloudfront.ts b/packages/ui/certd-server/src/plugins/plugin-aws-cn/plugins/plugin-deploy-to-cloudfront.ts new file mode 100644 index 00000000..dc4c7b0b --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-aws-cn/plugins/plugin-deploy-to-cloudfront.ts @@ -0,0 +1,165 @@ +import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; +import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert"; +import { AwsCNAccess, AwsCNRegions } from "../access.js"; +import { AwsIAMClient } from "../libs/aws-iam-client.js"; +import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; +import { optionsUtils } from "@certd/basic/dist/utils/util.options.js"; + +@IsTaskPlugin({ + name: 'AwsCNDeployToCloudFront', + title: 'AWS(国区)-部署证书到CloudFront', + desc: '部署证书到 AWS CloudFront', + icon: 'svg:icon-aws', + group: pluginGroups.aws.key, + needPlus: false, + default: { + strategy: { + runStrategy: RunStrategy.SkipWhenSucceed, + }, + }, +}) +export class AwsCNDeployToCloudFront extends AbstractTaskPlugin { + @TaskInput({ + title: '域名证书', + helper: '请选择前置任务输出的域名证书', + component: { + name: 'output-selector', + from: [...CertApplyPluginNames, 'AwsUploadToACM'], + }, + required: true, + }) + cert!: CertInfo | string; + + @TaskInput(createCertDomainGetterInputDefine({ props: { required: false } })) + certDomains!: string[]; + + @TaskInput({ + title: '区域', + helper: '证书上传区域', + component: { + name: 'a-auto-complete', + vModel: 'value', + options: AwsCNRegions, + }, + required: true, + }) + region!: string; + + @TaskInput({ + title: 'Access授权', + helper: 'aws的授权', + component: { + name: 'access-selector', + type: 'aws-cn', + }, + required: true, + }) + accessId!: string; + + @TaskInput({ + title: '证书名称', + helper: '上传后将以此名称作为前缀备注', + }) + certName!: string; + + @TaskInput( + createRemoteSelectInputDefine({ + title: '分配ID', + helper: '请选择distributions id', + action: AwsCNDeployToCloudFront.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.uploadToIAM(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, + IAMCertificateId: certId, + }, + }, + Id: distributionId, + IfMatch: configData.ETag, + }); + await cloudFrontClient.send(updateDistributionCommand); + this.logger.info(`部署${distributionId}完成:`); + } + this.logger.info('部署完成'); + } + + private async uploadToIAM(access: AwsCNAccess, cert: CertInfo) { + const acmClient = new AwsIAMClient({ + access, + region: this.region, + }); + const awsCertID = await acmClient.importCertificate(cert, this.appendTimeSuffix(this.certName)); + this.logger.info('证书上传成功,id=', awsCertID); + return awsCertID; + } + + //查找分配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 AwsCNDeployToCloudFront();