feat: support replacing old certificate on deployment to aws acm

This commit is contained in:
Fu Diwei 2025-05-15 22:09:32 +08:00
parent cd93a2d72c
commit 9e08cfd1d1
13 changed files with 89 additions and 17 deletions

View File

@ -306,6 +306,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
AccessKeyId: access.AccessKeyId, AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey, SecretAccessKey: access.SecretAccessKey,
Region: maputil.GetString(options.ProviderExtendedConfig, "region"), Region: maputil.GetString(options.ProviderExtendedConfig, "region"),
CertificateArn: maputil.GetString(options.ProviderExtendedConfig, "certificateArn"),
}) })
return deployer, err return deployer, err

View File

@ -5,9 +5,15 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
aws "github.com/aws/aws-sdk-go-v2/aws"
awscfg "github.com/aws/aws-sdk-go-v2/config"
awscred "github.com/aws/aws-sdk-go-v2/credentials"
awsacm "github.com/aws/aws-sdk-go-v2/service/acm"
"github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader" "github.com/usual2970/certimate/internal/pkg/core/uploader"
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aws-acm" uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aws-acm"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
) )
type DeployerConfig struct { type DeployerConfig struct {
@ -17,11 +23,15 @@ type DeployerConfig struct {
SecretAccessKey string `json:"secretAccessKey"` SecretAccessKey string `json:"secretAccessKey"`
// AWS 区域。 // AWS 区域。
Region string `json:"region"` Region string `json:"region"`
// ACM 证书 ARN。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateArn string `json:"certificateArn,omitempty"`
} }
type DeployerProvider struct { type DeployerProvider struct {
config *DeployerConfig config *DeployerConfig
logger *slog.Logger logger *slog.Logger
sdkClient *awsacm.Client
sslUploader uploader.Uploader sslUploader uploader.Uploader
} }
@ -32,6 +42,11 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
panic("config is nil") panic("config is nil")
} }
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: config.AccessKeyId, AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey, SecretAccessKey: config.SecretAccessKey,
@ -44,6 +59,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
return &DeployerProvider{ return &DeployerProvider{
config: config, config: config,
logger: slog.Default(), logger: slog.Default(),
sdkClient: client,
sslUploader: uploader, sslUploader: uploader,
}, nil }, nil
} }
@ -59,13 +75,48 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
} }
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 上传证书到 ACM if d.config.CertificateArn == "" {
upres, err := d.sslUploader.Upload(ctx, certPEM, privkeyPEM) // 上传证书到 ACM
if err != nil { upres, err := d.sslUploader.Upload(ctx, certPEM, privkeyPEM)
return nil, fmt.Errorf("failed to upload certificate file: %w", err) if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
} else { } else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) // 提取服务器证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 导入证书
// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ImportCertificate.html
importCertificateReq := &awsacm.ImportCertificateInput{
CertificateArn: aws.String(d.config.CertificateArn),
Certificate: ([]byte)(serverCertPEM),
CertificateChain: ([]byte)(intermediaCertPEM),
PrivateKey: ([]byte)(privkeyPEM),
}
importCertificateResp, err := d.sdkClient.ImportCertificate(context.TODO(), importCertificateReq)
d.logger.Debug("sdk request 'acm.ImportCertificate'", slog.Any("request", importCertificateReq), slog.Any("response", importCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'acm.ImportCertificate': %w", err)
}
} }
return &deployer.DeployResult{}, nil return &deployer.DeployResult{}, nil
} }
func createSdkClient(accessKeyId, secretAccessKey, region string) (*awsacm.Client, error) {
cfg, err := awscfg.LoadDefaultConfig(context.TODO())
if err != nil {
return nil, err
}
client := awsacm.NewFromConfig(cfg, func(o *awsacm.Options) {
o.Region = region
o.Credentials = aws.NewCredentialsCache(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, ""))
})
return client, nil
}

View File

@ -32,7 +32,7 @@ type DeployerConfig struct {
// Key Vault 名称。 // Key Vault 名称。
KeyVaultName string `json:"keyvaultName"` KeyVaultName string `json:"keyvaultName"`
// Key Vault 证书名称。 // Key Vault 证书名称。
// 选填。 // 选填。零值时表示新建证书;否则表示更新证书。
CertificateName string `json:"certificateName,omitempty"` CertificateName string `json:"certificateName,omitempty"`
} }

View File

@ -20,7 +20,7 @@ type DeployerConfig struct {
// 加速域名(支持泛域名)。 // 加速域名(支持泛域名)。
Domain string `json:"domain"` Domain string `json:"domain"`
// 证书 ID。 // 证书 ID。
// 选填。 // 选填。零值时表示新建证书;否则表示更新证书。
CertificateId string `json:"certificateId,omitempty"` CertificateId string `json:"certificateId,omitempty"`
} }

View File

@ -51,6 +51,7 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 上传证书 // 上传证书
// REF: https://api.cachefly.com/api/2.5/docs#tag/Certificates/paths/~1certificates/post
createCertificateReq := &cfsdk.CreateCertificateRequest{ createCertificateReq := &cfsdk.CreateCertificateRequest{
Certificate: certPEM, Certificate: certPEM,
CertificateKey: privkeyPEM, CertificateKey: privkeyPEM,

View File

@ -56,10 +56,10 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
} }
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 提取 Edgio 所需的服务端证书和中间证书内容 // 提取服务器证书和中间证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM) serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to extract certs: %w", err)
} }
// 上传 TLS 证书 // 上传 TLS 证书

View File

@ -24,7 +24,7 @@ type DeployerConfig struct {
// CDN 资源 ID。 // CDN 资源 ID。
ResourceId int64 `json:"resourceId"` ResourceId int64 `json:"resourceId"`
// 证书 ID。 // 证书 ID。
// 选填。 // 选填。零值时表示新建证书;否则表示更新证书。
CertificateId int64 `json:"certificateId,omitempty"` CertificateId int64 `json:"certificateId,omitempty"`
} }

View File

@ -34,7 +34,7 @@ type DeployerConfig struct {
// 加速域名(支持泛域名)。 // 加速域名(支持泛域名)。
Domain string `json:"domain"` Domain string `json:"domain"`
// 证书 ID。 // 证书 ID。
// 选填。 // 选填。零值时表示新建证书;否则表示更新证书。
CertificateId string `json:"certificateId,omitempty"` CertificateId string `json:"certificateId,omitempty"`
// Webhook ID。 // Webhook ID。
// 选填。 // 选填。

View File

@ -65,9 +65,11 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
return nil, err return nil, err
} }
// 生成 AWS 业务参数 // 提取服务器证书
scertPEM, _ := certutil.ConvertCertificateToPEM(certX509) serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
bcertPEM := certPEM if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 获取证书列表,避免重复上传 // 获取证书列表,避免重复上传
// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ListCertificates.html // REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ListCertificates.html
@ -145,8 +147,8 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
// 导入证书 // 导入证书
// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ImportCertificate.html // REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ImportCertificate.html
importCertificateReq := &awsacm.ImportCertificateInput{ importCertificateReq := &awsacm.ImportCertificateInput{
Certificate: ([]byte)(scertPEM), Certificate: ([]byte)(serverCertPEM),
CertificateChain: ([]byte)(bcertPEM), CertificateChain: ([]byte)(intermediaCertPEM),
PrivateKey: ([]byte)(privkeyPEM), PrivateKey: ([]byte)(privkeyPEM),
} }
importCertificateResp, err := u.sdkClient.ImportCertificate(context.TODO(), importCertificateReq) importCertificateResp, err := u.sdkClient.ImportCertificate(context.TODO(), importCertificateReq)

View File

@ -5,6 +5,7 @@ import { z } from "zod";
type DeployNodeConfigFormAWSACMConfigFieldValues = Nullish<{ type DeployNodeConfigFormAWSACMConfigFieldValues = Nullish<{
region: string; region: string;
certificateArn?: string;
}>; }>;
export type DeployNodeConfigFormAWSACMConfigProps = { export type DeployNodeConfigFormAWSACMConfigProps = {
@ -27,6 +28,7 @@ const DeployNodeConfigFormAWSACMConfig = ({ form: formInst, formName, disabled,
.string({ message: t("workflow_node.deploy.form.aws_acm_region.placeholder") }) .string({ message: t("workflow_node.deploy.form.aws_acm_region.placeholder") })
.nonempty(t("workflow_node.deploy.form.aws_acm_region.placeholder")) .nonempty(t("workflow_node.deploy.form.aws_acm_region.placeholder"))
.trim(), .trim(),
certificateArn: z.string({ message: t("workflow_node.deploy.form.aws_acm_certificate_arn.placeholder") }).nullish(),
}); });
const formRule = createSchemaFieldRule(formSchema); const formRule = createSchemaFieldRule(formSchema);
@ -51,6 +53,15 @@ const DeployNodeConfigFormAWSACMConfig = ({ form: formInst, formName, disabled,
> >
<Input placeholder={t("workflow_node.deploy.form.aws_acm_region.placeholder")} /> <Input placeholder={t("workflow_node.deploy.form.aws_acm_region.placeholder")} />
</Form.Item> </Form.Item>
<Form.Item
name="certificateArn"
label={t("workflow_node.deploy.form.aws_acm_certificate_arn.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aws_acm_certificate_arn.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aws_acm_certificate_arn.placeholder")} />
</Form.Item>
</Form> </Form>
); );
}; };

View File

@ -37,7 +37,7 @@ const DeployNodeConfigFormAzureKeyVaultConfig = ({
certificateName: z certificateName: z
.string({ message: t("workflow_node.deploy.form.azure_keyvault_certificate_name.placeholder") }) .string({ message: t("workflow_node.deploy.form.azure_keyvault_certificate_name.placeholder") })
.nullish() .nullish()
.refine((v) =>{ .refine((v) => {
if (!v) return true; if (!v) return true;
return /^[a-zA-Z0-9-]{1,127}$/.test(v); return /^[a-zA-Z0-9-]{1,127}$/.test(v);
}, t("workflow_node.deploy.form.azure_keyvault_certificate_name.errmsg.invalid")), }, t("workflow_node.deploy.form.azure_keyvault_certificate_name.errmsg.invalid")),

View File

@ -261,6 +261,9 @@
"workflow_node.deploy.form.aws_acm_region.label": "AWS ACM Region", "workflow_node.deploy.form.aws_acm_region.label": "AWS ACM Region",
"workflow_node.deploy.form.aws_acm_region.placeholder": "Please enter AWS ACM region (e.g. us-east-1)", "workflow_node.deploy.form.aws_acm_region.placeholder": "Please enter AWS ACM region (e.g. us-east-1)",
"workflow_node.deploy.form.aws_acm_region.tooltip": "For more information, see <a href=\"https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints\" target=\"_blank\">https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints</a>", "workflow_node.deploy.form.aws_acm_region.tooltip": "For more information, see <a href=\"https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints\" target=\"_blank\">https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints</a>",
"workflow_node.deploy.form.aws_acm_certificate_arn.label": "AWS ACM certificate ARN (Optional)",
"workflow_node.deploy.form.aws_acm_certificate_arn.placeholder": "Please enter AWS ACM certificate ARN",
"workflow_node.deploy.form.aws_acm_certificate_arn.tooltip": "Leave it blank to import a new certificate.",
"workflow_node.deploy.form.aws_cloudfront_region.label": "AWS CloudFront Region", "workflow_node.deploy.form.aws_cloudfront_region.label": "AWS CloudFront Region",
"workflow_node.deploy.form.aws_cloudfront_region.placeholder": "Please enter AWS CloudFront region (e.g. us-east-1)", "workflow_node.deploy.form.aws_cloudfront_region.placeholder": "Please enter AWS CloudFront region (e.g. us-east-1)",
"workflow_node.deploy.form.aws_cloudfront_region.tooltip": "For more information, see <a href=\"https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints\" target=\"_blank\">https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints</a>", "workflow_node.deploy.form.aws_cloudfront_region.tooltip": "For more information, see <a href=\"https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints\" target=\"_blank\">https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints</a>",

View File

@ -260,6 +260,9 @@
"workflow_node.deploy.form.aws_acm_region.label": "AWS ACM 服务区域", "workflow_node.deploy.form.aws_acm_region.label": "AWS ACM 服务区域",
"workflow_node.deploy.form.aws_acm_region.placeholder": "请输入 AWS ACM 服务区域例如us-east-1", "workflow_node.deploy.form.aws_acm_region.placeholder": "请输入 AWS ACM 服务区域例如us-east-1",
"workflow_node.deploy.form.aws_acm_region.tooltip": "这是什么?请参阅 <a href=\"https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints\" target=\"_blank\">https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints</a>", "workflow_node.deploy.form.aws_acm_region.tooltip": "这是什么?请参阅 <a href=\"https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints\" target=\"_blank\">https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints</a>",
"workflow_node.deploy.form.aws_acm_certificate_arn.label": "AWS ACM 证书 ARN可选",
"workflow_node.deploy.form.aws_acm_certificate_arn.placeholder": "请输入 AWS ACM 证书 ARN",
"workflow_node.deploy.form.aws_acm_certificate_arn.tooltip": "不填写时,将导入为新证书。",
"workflow_node.deploy.form.aws_cloudfront_region.label": "AWS CloudFront 服务区域", "workflow_node.deploy.form.aws_cloudfront_region.label": "AWS CloudFront 服务区域",
"workflow_node.deploy.form.aws_cloudfront_region.placeholder": "请输入 AWS CloudFront 服务区域例如us-east-1", "workflow_node.deploy.form.aws_cloudfront_region.placeholder": "请输入 AWS CloudFront 服务区域例如us-east-1",
"workflow_node.deploy.form.aws_cloudfront_region.tooltip": "这是什么?请参阅 <a href=\"https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints\" target=\"_blank\">https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints</a>", "workflow_node.deploy.form.aws_cloudfront_region.tooltip": "这是什么?请参阅 <a href=\"https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints\" target=\"_blank\">https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints</a>",