diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go
index 9d296065..d7fc67cd 100644
--- a/internal/deployer/deployer.go
+++ b/internal/deployer/deployer.go
@@ -19,6 +19,7 @@ const (
targetAliyunCDN = "aliyun-cdn"
targetAliyunESA = "aliyun-dcdn"
targetTencentCDN = "tencent-cdn"
+ targetTencentCLB = "tencent-clb"
targetTencentCOS = "tencent-cos"
targetHuaweiCloudCDN = "huaweicloud-cdn"
targetQiniuCdn = "qiniu-cdn"
@@ -106,6 +107,8 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
return NewAliyunESADeployer(option)
case targetTencentCDN:
return NewTencentCDNDeployer(option)
+ case targetTencentCLB:
+ return NewTencentCLBDeployer(option)
case targetTencentCOS:
return NewTencentCOSDeployer(option)
case targetHuaweiCloudCDN:
diff --git a/internal/deployer/tencent_clb.go b/internal/deployer/tencent_clb.go
new file mode 100644
index 00000000..4da6c34c
--- /dev/null
+++ b/internal/deployer/tencent_clb.go
@@ -0,0 +1,117 @@
+package deployer
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
+ "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
+ ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
+
+ "github.com/usual2970/certimate/internal/domain"
+ "github.com/usual2970/certimate/internal/utils/rand"
+)
+
+type TencentCLBDeployer struct {
+ option *DeployerOption
+ credential *common.Credential
+ infos []string
+}
+
+func NewTencentCLBDeployer(option *DeployerOption) (Deployer, error) {
+ access := &domain.TencentAccess{}
+ if err := json.Unmarshal([]byte(option.Access), access); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err)
+ }
+
+ credential := common.NewCredential(
+ access.SecretId,
+ access.SecretKey,
+ )
+
+ return &TencentCLBDeployer{
+ option: option,
+ credential: credential,
+ infos: make([]string, 0),
+ }, nil
+}
+
+func (d *TencentCLBDeployer) GetID() string {
+ return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
+}
+
+func (d *TencentCLBDeployer) GetInfo() []string {
+ return d.infos
+}
+
+func (d *TencentCLBDeployer) Deploy(ctx context.Context) error {
+ // 上传证书
+ certId, err := d.uploadCert()
+ if err != nil {
+ return fmt.Errorf("failed to upload certificate: %w", err)
+ }
+ d.infos = append(d.infos, toStr("上传证书", certId))
+
+ if err := d.deploy(certId); err != nil {
+ return fmt.Errorf("failed to deploy: %w", err)
+ }
+
+ return nil
+}
+
+func (d *TencentCLBDeployer) uploadCert() (string, error) {
+ cpf := profile.NewClientProfile()
+ cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
+
+ client, _ := ssl.NewClient(d.credential, "", cpf)
+
+ request := ssl.NewUploadCertificateRequest()
+
+ request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate)
+ request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey)
+ request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6))
+ request.Repeatable = common.BoolPtr(false)
+
+ response, err := client.UploadCertificate(request)
+ if err != nil {
+ return "", fmt.Errorf("failed to upload certificate: %w", err)
+ }
+
+ return *response.Response.CertificateId, nil
+}
+
+func (d *TencentCLBDeployer) deploy(certId string) error {
+ cpf := profile.NewClientProfile()
+ cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
+ // 实例化要请求产品的client对象,clientProfile是可选的
+ client, _ := ssl.NewClient(d.credential, "", cpf)
+
+ // 实例化一个请求对象,每个接口都会对应一个request对象
+ request := ssl.NewDeployCertificateInstanceRequest()
+
+ request.CertificateId = common.StringPtr(certId)
+ request.ResourceType = common.StringPtr("cdn")
+ request.Status = common.Int64Ptr(1)
+
+ clbId := getDeployString(d.option.DeployConfig, "clbId")
+ lsnId := getDeployString(d.option.DeployConfig, "lsnId")
+ domain := getDeployString(d.option.DeployConfig, "domain")
+
+ if(domain == ""){
+ // 未开启SNI,只需要精确到监听器
+ request.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s", clbId, lsnId)})
+ }else{
+ // 开启SNI,需要精确到域名,支持泛域名
+ request.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", clbId, lsnId, domain)})
+ }
+
+
+ // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应
+ resp, err := client.DeployCertificateInstance(request)
+ if err != nil {
+ return fmt.Errorf("failed to deploy certificate: %w", err)
+ }
+ d.infos = append(d.infos, toStr("部署证书", resp.Response))
+ return nil
+}
\ No newline at end of file
diff --git a/ui/src/components/certimate/DeployEditDialog.tsx b/ui/src/components/certimate/DeployEditDialog.tsx
index 54710e49..24149ff5 100644
--- a/ui/src/components/certimate/DeployEditDialog.tsx
+++ b/ui/src/components/certimate/DeployEditDialog.tsx
@@ -12,6 +12,7 @@ import { Context as DeployEditContext } from "./DeployEdit";
import DeployToAliyunOSS from "./DeployToAliyunOSS";
import DeployToAliyunCDN from "./DeployToAliyunCDN";
import DeployToTencentCDN from "./DeployToTencentCDN";
+import DeployToTencentCLB from "./DeployToTencentCLB";
import DeployToTencentCOS from "./DeployToTencentCOS";
import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN";
import DeployToQiniuCDN from "./DeployToQiniuCDN";
@@ -119,6 +120,9 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
case "tencent-cdn":
childComponent = ;
break;
+ case "tencent-clb":
+ childComponent = ;
+ break;
case "tencent-cos":
childComponent = ;
break;
diff --git a/ui/src/components/certimate/DeployToTencentCLB.tsx b/ui/src/components/certimate/DeployToTencentCLB.tsx
new file mode 100644
index 00000000..1a7a2eb6
--- /dev/null
+++ b/ui/src/components/certimate/DeployToTencentCLB.tsx
@@ -0,0 +1,197 @@
+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";
+
+const DeployToTencentCLB = () => {
+ const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
+
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ setError({});
+ }, []);
+
+ useEffect(() => {
+ const resp = domainSchema.safeParse(data.config?.domain);
+ if (!resp.success) {
+ setError({
+ ...error,
+ domain: JSON.parse(resp.error.message)[0].message,
+ });
+ } else {
+ setError({
+ ...error,
+ domain: "",
+ });
+ }
+ }, [data]);
+
+ useEffect(() => {
+ const clbIdresp = clbIdSchema.safeParse(data.config?.clbId);
+ if (!clbIdresp.success) {
+ setError({
+ ...error,
+ clbId: JSON.parse(clbIdresp.error.message)[0].message,
+ });
+ } else {
+ setError({
+ ...error,
+ clbId: "",
+ });
+ }
+ }, [data]);
+
+ useEffect(() => {
+ const lsnIdresp = lsnIdSchema.safeParse(data.config?.lsnId);
+ if (!lsnIdresp.success) {
+ setError({
+ ...error,
+ lsnId: JSON.parse(lsnIdresp.error.message)[0].message,
+ });
+ } else {
+ setError({
+ ...error,
+ lsnId: "",
+ });
+ }
+ }, [data]);
+
+
+ useEffect(() => {
+ if (!data.id) {
+ setDeploy({
+ ...data,
+ config: {
+ lsnId: "",
+ clbId: "",
+ domain: "",
+ },
+ });
+ }
+ }, []);
+
+ const domainSchema = z.string().regex(/^$|^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
+ message: t("common.errmsg.domain_invalid"),
+ });
+
+ const clbIdSchema = z.string().regex(/^lb-[a-zA-Z0-9]{8}$/, {
+ message: t("domain.deployment.form.clb_id.placeholder"),
+ });
+
+ const lsnIdSchema = z.string().regex(/^lbl-.{8}$/, {
+ message: t("domain.deployment.form.clb_listener.placeholder"),
+ });
+
+ return (
+
+
+
+
{
+ const temp = e.target.value;
+
+ const resp = clbIdSchema.safeParse(temp);
+ if (!resp.success) {
+ setError({
+ ...error,
+ clbId: JSON.parse(resp.error.message)[0].message,
+ });
+ } else {
+ setError({
+ ...error,
+ clbId: "",
+ });
+ }
+
+ const newData = produce(data, (draft) => {
+ if (!draft.config) {
+ draft.config = {};
+ }
+ draft.config.clbId = temp;
+ });
+ setDeploy(newData);
+ }}
+ />
+
{error?.clbId}
+
+
+
+
+
{
+ const temp = e.target.value;
+
+ const resp = lsnIdSchema.safeParse(temp);
+ if (!resp.success) {
+ setError({
+ ...error,
+ lsnId: JSON.parse(resp.error.message)[0].message,
+ });
+ } else {
+ setError({
+ ...error,
+ lsnId: "",
+ });
+ }
+
+ const newData = produce(data, (draft) => {
+ if (!draft.config) {
+ draft.config = {};
+ }
+ draft.config.lsnId = temp;
+ });
+ setDeploy(newData);
+ }}
+ />
+
{error?.lsnId}
+
+
+
+
+
{
+ const temp = e.target.value;
+
+ const resp = domainSchema.safeParse(temp);
+ if (!resp.success) {
+ setError({
+ ...error,
+ domain: JSON.parse(resp.error.message)[0].message,
+ });
+ } else {
+ setError({
+ ...error,
+ domain: "",
+ });
+ }
+
+ const newData = produce(data, (draft) => {
+ if (!draft.config) {
+ draft.config = {};
+ }
+ draft.config.domain = temp;
+ });
+ setDeploy(newData);
+ }}
+ />
+
{error?.domain}
+
+
+ );
+};
+
+export default DeployToTencentCLB;
diff --git a/ui/src/domain/domain.ts b/ui/src/domain/domain.ts
index 350f89a4..cc67a54d 100644
--- a/ui/src/domain/domain.ts
+++ b/ui/src/domain/domain.ts
@@ -75,6 +75,7 @@ export const deployTargetsMap: Map = new Map
["aliyun-cdn", "common.provider.aliyun.cdn", "/imgs/providers/aliyun.svg"],
["aliyun-dcdn", "common.provider.aliyun.dcdn", "/imgs/providers/aliyun.svg"],
["tencent-cdn", "common.provider.tencent.cdn", "/imgs/providers/tencent.svg"],
+ ["tencent-clb", "common.provider.tencent.clb", "/imgs/providers/tencent.svg"],
["tencent-cos", "common.provider.tencent.cos", "/imgs/providers/tencent.svg"],
["huaweicloud-cdn", "common.provider.huaweicloud.cdn", "/imgs/providers/huaweicloud.svg"],
["qiniu-cdn", "common.provider.qiniu.cdn", "/imgs/providers/qiniu.svg"],
diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json
index d3a17019..0f142747 100644
--- a/ui/src/i18n/locales/en/nls.common.json
+++ b/ui/src/i18n/locales/en/nls.common.json
@@ -58,6 +58,7 @@
"common.provider.aliyun.dcdn": "Alibaba Cloud - DCDN",
"common.provider.tencent": "Tencent",
"common.provider.tencent.cdn": "Tencent - CDN",
+ "common.provider.tencent.clb": "Tencent - CLB",
"common.provider.tencent.cos": "Tencent - COS",
"common.provider.huaweicloud": "Huawei Cloud",
"common.provider.huaweicloud.cdn": "Huawei Cloud - CDN",
diff --git a/ui/src/i18n/locales/en/nls.domain.json b/ui/src/i18n/locales/en/nls.domain.json
index 15f57284..745ff670 100644
--- a/ui/src/i18n/locales/en/nls.domain.json
+++ b/ui/src/i18n/locales/en/nls.domain.json
@@ -58,6 +58,12 @@
"domain.deployment.form.cos_region.placeholder": "Please enter region, e.g. ap-guangzhou",
"domain.deployment.form.cos_bucket.label": "Bucket",
"domain.deployment.form.cos_bucket.placeholder": "Please enter bucket, e.g. example-1250000000",
+ "domain.deployment.form.clb_id.label": "CLB id",
+ "domain.deployment.form.clb_id.placeholder": "Please enter CLB id, e.g. lb-xxxxxxxx",
+ "domain.deployment.form.clb_listener.label": "Listener id",
+ "domain.deployment.form.clb_listener.placeholder": "Please enter listener id, e.g. lbl-xxxxxxxx",
+ "domain.deployment.form.clb_domain.label": "Deploy to domain (Wildcard domain is also supported)",
+ "domain.deployment.form.clb_domain.placeholder": "Please enter domain to be deployed. If SNI is not enabled, you can leave it blank.",
"domain.deployment.form.domain.label": "Deploy to domain (Single domain only, not wildcard domain)",
"domain.deployment.form.domain.label.wildsupported": "Deploy to domain (Wildcard domain is also supported)",
"domain.deployment.form.domain.placeholder": "Please enter domain to be deployed",
diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json
index 212d7b2e..6a27105c 100644
--- a/ui/src/i18n/locales/zh/nls.common.json
+++ b/ui/src/i18n/locales/zh/nls.common.json
@@ -54,6 +54,7 @@
"common.provider.tencent": "腾讯云",
"common.provider.tencent.cdn": "腾讯云 - CDN",
+ "common.provider.tencent.clb": "腾讯云 - CLB",
"common.provider.tencent.cos": "腾讯云 - COS",
"common.provider.aliyun": "阿里云",
"common.provider.aliyun.oss": "阿里云 - OSS",
diff --git a/ui/src/i18n/locales/zh/nls.domain.json b/ui/src/i18n/locales/zh/nls.domain.json
index d7dbadbc..b271def7 100644
--- a/ui/src/i18n/locales/zh/nls.domain.json
+++ b/ui/src/i18n/locales/zh/nls.domain.json
@@ -58,6 +58,12 @@
"domain.deployment.form.cos_region.placeholder": "请输入 region, 如 ap-guangzhou",
"domain.deployment.form.cos_bucket.label": "存储桶",
"domain.deployment.form.cos_bucket.placeholder": "请输入存储桶名, 如 example-1250000000",
+ "domain.deployment.form.clb_id.label": "CLB id",
+ "domain.deployment.form.clb_id.placeholder": "请输入CLB实例id, 如 lb-xxxxxxxx",
+ "domain.deployment.form.clb_listener.label": "监听器 id",
+ "domain.deployment.form.clb_listener.placeholder": "请输入监听器id, 如 lbl-xxxxxxxx",
+ "domain.deployment.form.clb_domain.label": "部署到域名(支持泛域名)",
+ "domain.deployment.form.clb_domain.placeholder": "请输入部署到的域名, 如未开启SNI, 可置空忽略此项",
"domain.deployment.form.domain.label": "部署到域名(仅支持单个域名;不支持泛域名)",
"domain.deployment.form.domain.label.wildsupported": "部署到域名(支持泛域名)",
"domain.deployment.form.domain.placeholder": "请输入部署到的域名",