feat: add volcengine cdn deployer

This commit is contained in:
belier 2024-11-14 13:39:23 +08:00
parent 2bacf76664
commit 9eae8f5077
10 changed files with 309 additions and 4 deletions

View File

@ -79,7 +79,7 @@ make local.run
| 华为云 | √ | √ | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB |
| 七牛云 | | √ | 可部署到七牛云 CDN |
| 多吉云 | | √ | 可部署到多吉云 CDN |
| 火山引擎 | √ | √ | 可签发在火山引擎注册的域名;可部署到火山引擎 Live |
| 火山引擎 | √ | √ | 可签发在火山引擎注册的域名;可部署到火山引擎 Live、CDN |
| AWS | √ | | 可签发在 AWS Route53 托管的域名 |
| CloudFlare | √ | | 可签发在 CloudFlare 注册的域名CloudFlare 服务自带 SSL 证书 |
| GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 |

View File

@ -78,7 +78,7 @@ password1234567890
| Huawei Cloud | √ | √ | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN, ELB |
| Qiniu Cloud | | √ | Supports deployment to Qiniu Cloud CDN |
| Doge Cloud | | √ | Supports deployment to Doge Cloud CDN |
| Volcengine | √ | √ | Supports domains registered on Volcengine; supports deployment to Volcengine Live |
| Volcengine | √ | √ | Supports domains registered on Volcengine; supports deployment to Volcengine Live、CDN |
| AWS | √ | | Supports domains managed on AWS Route53 |
| CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates |
| GoDaddy | √ | | Supports domains registered on GoDaddy |

View File

@ -41,6 +41,7 @@ const (
targetWebhook = "webhook"
targetK8sSecret = "k8s-secret"
targetVolcengineLive = "volcengine-live"
targetVolcengineCDN = "volcengine-cdn"
)
type DeployerOption struct {
@ -153,6 +154,8 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
return NewK8sSecretDeployer(option)
case targetVolcengineLive:
return NewVolcengineLiveDeployer(option)
case targetVolcengineCDN:
return NewVolcengineCDNDeployer(option)
}
return nil, errors.New("unsupported deploy target")
}

View File

@ -0,0 +1,116 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"strings"
volcenginecdn "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-cdn"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/volcengine/volc-sdk-golang/service/cdn"
)
type VolcengineCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *cdn.CDN
sslUploader uploader.Uploader
}
func NewVolcengineCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.VolcengineAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client := cdn.NewInstance()
client.Client.SetAccessKey(access.AccessKeyID)
client.Client.SetSecretKey(access.SecretAccessKey)
uploader, err := volcenginecdn.New(&volcenginecdn.VolcengineCDNUploaderConfig{
AccessKeyId: access.AccessKeyID,
AccessKeySecret: access.SecretAccessKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &VolcengineCDNDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *VolcengineCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *VolcengineCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *VolcengineCDNDeployer) Deploy(ctx context.Context) error {
apiCtx := context.Background()
// 上传证书
upres, err := d.sslUploader.Upload(apiCtx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
domains := make([]string, 0)
configDomain := d.option.DeployConfig.GetConfigAsString("domain")
if strings.HasPrefix(configDomain, "*.") {
// 获取证书可以部署的域名
// REF: https://www.volcengine.com/docs/6454/125711
describeCertConfigReq := &cdn.DescribeCertConfigRequest{
CertId: upres.CertId,
}
describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertConfig'")
}
for i := range describeCertConfigResp.Result.CertNotConfig {
// 当前未启用 HTTPS 的加速域名列表。
domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain)
}
for i := range describeCertConfigResp.Result.OtherCertConfig {
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联的证书不是您指定的证书。
domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain)
}
for i := range describeCertConfigResp.Result.SpecifiedCertConfig {
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联了您指定的证书。
d.infos = append(d.infos, fmt.Sprintf("%s域名已配置该证书", describeCertConfigResp.Result.SpecifiedCertConfig[i].Domain))
}
if len(domains) == 0 {
if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 {
// 所有匹配的域名都配置了该证书,跳过部署
return nil
} else {
return xerrors.Errorf("未查询到匹配的域名: %s", configDomain)
}
}
} else {
domains = append(domains, configDomain)
}
// 部署证书
// REF: https://www.volcengine.com/docs/6454/125712
for i := range domains {
batchDeployCertReq := &cdn.BatchDeployCertRequest{
CertId: upres.CertId,
Domain: domains[i],
}
BindCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.BatchDeployCert'")
} else {
d.infos = append(d.infos, toStr(fmt.Sprintf("%s域名的证书已修改", domains[i]), BindCertResp))
}
}
return nil
}

View File

@ -0,0 +1,111 @@
package volcenginecdn
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"time"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
"github.com/volcengine/volc-sdk-golang/service/cdn"
)
type VolcengineCDNUploaderConfig struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type VolcengineCDNUploader struct {
config *VolcengineCDNUploaderConfig
sdkClient *cdn.CDN
}
var _ uploader.Uploader = (*VolcengineCDNUploader)(nil)
func New(config *VolcengineCDNUploaderConfig) (*VolcengineCDNUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
instance := cdn.NewInstance()
client := instance.Client
client.SetAccessKey(config.AccessKeyId)
client.SetSecretKey(config.AccessKeySecret)
return &VolcengineCDNUploader{
config: config,
sdkClient: instance,
}, nil
}
func (u *VolcengineCDNUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://www.volcengine.com/docs/6454/125709
pageNum := int64(1)
pageSize := int64(100)
certSource := "volc_cert_center"
listCertReq := &cdn.ListCertInfoRequest{
PageNum: &pageNum,
PageSize: &pageSize,
Source: certSource,
}
searchTotal := 0
for {
listCertResp, err := u.sdkClient.ListCertInfo(listCertReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ListCertInfo'")
}
if listCertResp.Result.CertInfo != nil {
for _, certDetail := range listCertResp.Result.CertInfo {
hash := sha256.Sum256(certX509.Raw)
isSameCert := hex.EncodeToString(hash[:]) == certDetail.CertFingerprint.Sha256
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &uploader.UploadResult{
CertId: certDetail.CertId,
CertName: certDetail.Desc,
}, nil
}
}
}
searchTotal += len(listCertResp.Result.CertInfo)
if int(listCertResp.Result.Total) > searchTotal {
pageNum++
} else {
break
}
}
// 生成新证书名(需符合火山引擎命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://www.volcengine.com/docs/6454/1245763
addCertificateReq := &cdn.AddCertificateRequest{
Certificate: certPem,
PrivateKey: privkeyPem,
Source: &certSource,
Desc: &certName,
}
createCertResp, err := u.sdkClient.AddCertificate(addCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.AddCertificate'")
}
certId = createCertResp.Result.CertId
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}

View File

@ -28,6 +28,7 @@ import DeployToSSH from "./DeployToSSH";
import DeployToWebhook from "./DeployToWebhook";
import DeployToKubernetesSecret from "./DeployToKubernetesSecret";
import DeployToVolcengineLive from "./DeployToVolcengineLive"
import DeployToVolcengineCDN from "./DeployToVolcengineCDN"
import { deployTargetsMap, type DeployConfig } from "@/domain/domain";
import { accessProvidersMap } from "@/domain/access";
import { useConfigContext } from "@/providers/config";
@ -178,6 +179,9 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
case "volcengine-live":
childComponent = <DeployToVolcengineLive />;
break;
case "volcengine-cdn":
childComponent = <DeployToVolcengineCDN />;
break;
}
return (

View File

@ -0,0 +1,68 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { produce } from "immer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
type DeployToVolcengineCDNConfigParams = {
domain?: string;
};
const DeployToVolcengineCDN = () => {
const { t } = useTranslation();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToVolcengineCDNConfigParams>();
useEffect(() => {
if (!config.id) {
setConfig({
...config,
config: {},
});
}
}, []);
useEffect(() => {
setErrors({});
}, []);
const formSchema = z.object({
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
useEffect(() => {
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label.wildsupported")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToVolcengineCDN;

View File

@ -93,5 +93,6 @@ export const deployTargetsMap: Map<DeployTarget["type"], DeployTarget> = new Map
["webhook", "common.provider.webhook", "/imgs/providers/webhook.svg"],
["k8s-secret", "common.provider.kubernetes.secret", "/imgs/providers/k8s.svg"],
["volcengine-live", "common.provider.volcengine.live", "/imgs/providers/volcengine.svg"],
["volcengine-cdn", "common.provider.volcengine.cdn", "/imgs/providers/volcengine.svg"],
].map(([type, name, icon]) => [type, { type, provider: type.split("-")[0], name, icon }])
);

View File

@ -92,5 +92,6 @@
"common.provider.serverchan": "ServerChan",
"common.provider.bark": "Bark",
"common.provider.volcengine": "Volcengine",
"common.provider.volcengine.live": "Volcengine - Live"
"common.provider.volcengine.live": "Volcengine - Live",
"common.provider.volcengine.cdn": "Volcengine - CDN"
}

View File

@ -92,5 +92,6 @@
"common.provider.serverchan": "Server酱",
"common.provider.bark": "Bark",
"common.provider.volcengine": "火山引擎",
"common.provider.volcengine.live": "火山引擎 - 视频直播"
"common.provider.volcengine.live": "火山引擎 - 视频直播",
"common.provider.volcengine.cdn": "火山引擎 - CDN"
}