feat(provider): support multiple certificates in deployment to tencentcloud edgeone

This commit is contained in:
Fu Diwei
2025-11-20 20:16:56 +08:00
parent e363ad24b1
commit 3fb8c2e7a8
8 changed files with 154 additions and 30 deletions
@@ -26,6 +26,7 @@ func init() {
ZoneId: xmaps.GetString(options.ProviderExtendedConfig, "zoneId"),
DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"),
Domains: lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, "domains"), ";"), func(s string, _ int) bool { return s != "" }),
EnableMultipleSSL: xmaps.GetBool(options.ProviderExtendedConfig, "enableMultipleSSL"),
})
return provider, err
})
@@ -35,7 +35,7 @@ func init() {
{
if nodeCfg["providerConfig"] != nil {
providerCfg := nodeCfg["providerConfig"].(map[string]any)
if providerCfg["websiteId"] != nil && providerCfg["websiteId"] != "" {
if providerCfg["websiteId"] != nil && providerCfg["websiteId"].(string) != "" {
providerCfg["websiteMatchPattern"] = "specified"
nodeCfg["providerConfig"] = providerCfg
@@ -76,7 +76,7 @@ func init() {
{
if nodeCfg["providerConfig"] != nil {
providerCfg := nodeCfg["providerConfig"].(map[string]any)
if providerCfg["siteName"] != nil && providerCfg["siteName"] != "" {
if providerCfg["siteNames"] == nil || providerCfg["siteNames"].(string) == "" {
providerCfg["siteType"] = "php"
providerCfg["siteNames"] = providerCfg["siteName"]
delete(providerCfg, "siteName")
@@ -123,7 +123,9 @@ func init() {
case "safeline":
{
node.Data["provider"] = "safeline-site"
nodeCfg["provider"] = "safeline-site"
node.Data["config"] = nodeCfg
_changed = true
return
}
@@ -2,10 +2,12 @@ package tencentcloudeo
import (
"context"
"crypto/x509"
"errors"
"fmt"
"log/slog"
"strings"
"time"
"github.com/samber/lo"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
@@ -18,6 +20,7 @@ import (
"github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-eo/internal"
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname"
xcryptokey "github.com/certimate-go/certimate/pkg/utils/crypto/key"
)
type DeployerConfig struct {
@@ -34,6 +37,8 @@ type DeployerConfig struct {
DomainMatchPattern string `json:"domainMatchPattern,omitempty"`
// 加速域名列表(支持泛域名)。
Domains []string `json:"domains"`
// 是否启用多证书模式。
EnableMultipleSSL bool `json:"enableMultipleSSL,omitempty"`
}
type Deployer struct {
@@ -97,6 +102,12 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*dep
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
// 获取全部可部署的域名信息
domainsInZone, err := d.getAllDomainsInZone(ctx, d.config.ZoneId)
if err != nil {
return nil, err
}
// 获取待部署的域名列表
var domains []string
switch d.config.DomainMatchPattern {
@@ -115,11 +126,9 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*dep
return nil, errors.New("config `domains` is required")
}
domainCandidates, err := d.getAllDomainsInZone(ctx, d.config.ZoneId)
if err != nil {
return nil, err
}
domainCandidates := lo.Map(domainsInZone, func(domainInfo *tcteo.AccelerationDomain, _ int) string {
return lo.FromPtr(domainInfo.DomainName)
})
domains = lo.Filter(domainCandidates, func(domain string, _ int) bool {
for _, configDomain := range d.config.Domains {
if xcerthostname.IsMatch(configDomain, domain) {
@@ -140,11 +149,9 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*dep
return nil, err
}
domainCandidates, err := d.getAllDomainsInZone(ctx, d.config.ZoneId)
if err != nil {
return nil, err
}
domainCandidates := lo.Map(domainsInZone, func(domainInfo *tcteo.AccelerationDomain, _ int) string {
return lo.FromPtr(domainInfo.DomainName)
})
domains = lo.Filter(domainCandidates, func(domain string, _ int) bool {
return certX509.VerifyHostname(domain) == nil
})
@@ -157,24 +164,110 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*dep
return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern)
}
// 配置域名证书
// REF: https://cloud.tencent.com/document/api/1552/80764
modifyHostsCertificateReq := tcteo.NewModifyHostsCertificateRequest()
modifyHostsCertificateReq.ZoneId = common.StringPtr(d.config.ZoneId)
modifyHostsCertificateReq.Mode = common.StringPtr("sslcert")
modifyHostsCertificateReq.Hosts = common.StringPtrs(domains)
modifyHostsCertificateReq.ServerCertInfo = []*tcteo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}}
modifyHostsCertificateResp, err := d.sdkClient.ModifyHostsCertificate(modifyHostsCertificateReq)
d.logger.Debug("sdk request 'teo.ModifyHostsCertificate'", slog.Any("request", modifyHostsCertificateReq), slog.Any("response", modifyHostsCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'teo.ModifyHostsCertificate': %w", err)
// 跳过已部署过的域名
domains = lo.Filter(domains, func(domain string, _ int) bool {
var deployed bool
domainInfo, _ := lo.Find(domainsInZone, func(domainInfo *tcteo.AccelerationDomain) bool {
return domain == lo.FromPtr(domainInfo.DomainName)
})
if domainInfo != nil && domainInfo.Certificate != nil {
deployed = lo.ContainsBy(domainInfo.Certificate.List, func(certInfo *tcteo.CertificateInfo) bool {
return upres.CertId == lo.FromPtr(certInfo.CertId)
})
}
return !deployed
})
// 批量更新域名证书
if len(domains) == 0 {
d.logger.Info("no edgeone domains to deploy")
} else {
d.logger.Info("found edgeone domains to deploy", slog.Any("domains", domains))
// 配置域名证书
// REF: https://cloud.tencent.com/document/api/1552/80764
modifyHostsCertificateReqs := make([]*tcteo.ModifyHostsCertificateRequest, 0)
if d.config.EnableMultipleSSL {
const algRSA = "RSA"
const algECC = "ECC"
privkeyAlg, _, _ := xcryptokey.GetPrivateKeyAlgorithm(privkeyPEM)
privkeyAlgStr := ""
switch privkeyAlg {
case x509.RSA:
privkeyAlgStr = algRSA
case x509.ECDSA:
privkeyAlgStr = algECC
}
for _, domain := range domains {
modifyHostsCertificateReq := tcteo.NewModifyHostsCertificateRequest()
modifyHostsCertificateReq.ZoneId = common.StringPtr(d.config.ZoneId)
modifyHostsCertificateReq.Mode = common.StringPtr("sslcert")
modifyHostsCertificateReq.Hosts = common.StringPtrs([]string{domain})
modifyHostsCertificateReq.ServerCertInfo = []*tcteo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}}
domainInfo, _ := lo.Find(domainsInZone, func(domainInfo *tcteo.AccelerationDomain) bool {
return domain == lo.FromPtr(domainInfo.DomainName)
})
if domainInfo != nil && domainInfo.Certificate != nil {
for _, certInfo := range domainInfo.Certificate.List {
if lo.FromPtr(certInfo.CertId) == upres.CertId {
continue
}
if strings.Split(lo.FromPtr(certInfo.SignAlgo), " ")[0] == privkeyAlgStr {
continue
}
certExpireTime, _ := time.Parse("2006-01-02T15:04:05Z", lo.FromPtr(certInfo.ExpireTime))
if certExpireTime.Before(time.Now()) {
continue
}
modifyHostsCertificateReq.ServerCertInfo = append(modifyHostsCertificateReq.ServerCertInfo, &tcteo.ServerCertInfo{CertId: certInfo.CertId})
}
}
modifyHostsCertificateReqs = append(modifyHostsCertificateReqs, modifyHostsCertificateReq)
}
} else {
modifyHostsCertificateReq := tcteo.NewModifyHostsCertificateRequest()
modifyHostsCertificateReq.ZoneId = common.StringPtr(d.config.ZoneId)
modifyHostsCertificateReq.Mode = common.StringPtr("sslcert")
modifyHostsCertificateReq.Hosts = common.StringPtrs(domains)
modifyHostsCertificateReq.ServerCertInfo = []*tcteo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}}
modifyHostsCertificateReqs = append(modifyHostsCertificateReqs, modifyHostsCertificateReq)
}
var errs []error
for _, modifyHostsCertificateReq := range modifyHostsCertificateReqs {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
modifyHostsCertificateResp, err := d.sdkClient.ModifyHostsCertificate(modifyHostsCertificateReq)
d.logger.Debug("sdk request 'teo.ModifyHostsCertificate'", slog.Any("request", modifyHostsCertificateReq), slog.Any("response", modifyHostsCertificateResp))
if err != nil {
err = fmt.Errorf("failed to execute sdk request 'teo.ModifyHostsCertificate': %w", err)
errs = append(errs, err)
}
}
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
}
return &deployer.DeployResult{}, nil
}
func (d *Deployer) getAllDomainsInZone(ctx context.Context, zoneId string) ([]string, error) {
var domainsInZone []string
func (d *Deployer) getAllDomainsInZone(ctx context.Context, zoneId string) ([]*tcteo.AccelerationDomain, error) {
var domainsInZone []*tcteo.AccelerationDomain
const pageSize = 200
for offset := 0; ; offset += pageSize {
@@ -196,12 +289,13 @@ func (d *Deployer) getAllDomainsInZone(ctx context.Context, zoneId string) ([]st
return nil, fmt.Errorf("failed to execute sdk request 'teo.DescribeAccelerationDomains': %w", err)
}
ignoredStatuses := []string{"offline", "forbidden", "init"}
for _, domainItem := range describeAccelerationDomainsResp.Response.AccelerationDomains {
if domainItem == nil || domainItem.DomainName == nil {
if lo.Contains(ignoredStatuses, lo.FromPtr(domainItem.DomainStatus)) {
continue
}
domainsInZone = append(domainsInZone, *domainItem.DomainName)
domainsInZone = append(domainsInZone, domainItem)
}
if len(describeAccelerationDomainsResp.Response.AccelerationDomains) < pageSize {
@@ -62,6 +62,7 @@ func TestDeploy(t *testing.T) {
ZoneId: fZoneId,
DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT,
Domains: strings.Split(fDomains, ";"),
EnableMultipleSSL: true,
})
if err != nil {
t.Errorf("err: %+v", err)
@@ -1,5 +1,5 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, Radio } from "antd";
import { Flex, Form, Input, Radio, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
@@ -88,6 +88,21 @@ const BizDeployNodeConfigFieldsProviderTencentCloudEO = () => {
/>
</Form.Item>
</Show>
<Form.Item
label={t("workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.label")}
extra={t("workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.help")}
>
<Flex align="center" gap={8} wrap="wrap">
<Form.Item name={[parentNamePath, "enableMultipleSSL"]} initialValue={initialValues.enableMultipleSSL} noStyle rules={[formRule]}>
<Switch
checkedChildren={t("workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.on")}
unCheckedChildren={t("workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.off")}
/>
</Form.Item>
<div>{t("workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.suffix")}</div>
</Flex>
</Form.Item>
</>
);
};
@@ -109,6 +124,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
zoneId: z.string().nonempty(t("workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder")),
domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT),
domains: z.string().nullish(),
enableMultipleSSL: z.boolean().nullish(),
})
.superRefine((values, ctx) => {
if (values.domainMatchPattern) {
@@ -817,6 +817,11 @@
"workflow_node.deploy.form.tencentcloud_eo_domains.help": "Notes: Multi-domains should be separated by semicolons.",
"workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.title": "Change Tencent Cloud EdgeOne domain",
"workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.placeholder": "Please enter Tencent Cloud EdgeOne domain name",
"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.label": "Multiple SSL certificates",
"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.help": "Notes: Each domain name supports one RSA certificate and one ECC certificate at most.",
"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.suffix": "to retain other certificates with different algorithms.",
"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.on": "Allow",
"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.off": "Disallow",
"workflow_node.deploy.form.tencentcloud_gaap_endpoint.label": "Tencent Cloud API endpoint (Optional)",
"workflow_node.deploy.form.tencentcloud_gaap_endpoint.placeholder": "Please enter Tencent Cloud GAAP API endpoint (e.g. gaap.intl.tencentcloudapi.com)",
"workflow_node.deploy.form.tencentcloud_gaap_endpoint.tooltip": "<ul style=\"list-style: disc;\"><li><strong>gaap.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>gaap.tencentcloudapi.com</strong> for Tencent Cloud in China</li></ul>",
+1 -1
View File
@@ -48,7 +48,7 @@
"access.form.shared_acme_eab_hmac_key.placeholder": "请输入 ACME EAB HMAC Key",
"access.form.shared_allow_insecure_conns.label": "忽略 SSL/TLS 证书错误",
"access.form.shared_allow_insecure_conns.switch.on": "允许",
"access.form.shared_allow_insecure_conns.switch.off": "不允许",
"access.form.shared_allow_insecure_conns.switch.off": "禁止",
"access.form.1panel_server_url.label": "1Panel 服务地址",
"access.form.1panel_server_url.placeholder": "请输入 1Panel 服务地址",
"access.form.1panel_server_url.help": "提示:请勿包含安全入口后缀。",
@@ -815,6 +815,11 @@
"workflow_node.deploy.form.tencentcloud_eo_domains.help": "提示:支持多个域名,以半角分号隔开。",
"workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.title": "修改腾讯云 EdgeOne 加速域名",
"workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.placeholder": "请输入腾讯云 EdgeOne 加速域名",
"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.label": "多证书模式",
"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.help": "提示:每个域名最多支持一个 RSA 证书、一个 ECC 证书。",
"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.suffix": "保留与待部署证书算法不一致的其他有效证书。",
"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.on": "",
"workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.off": "不",
"workflow_node.deploy.form.tencentcloud_gaap_endpoint.label": "腾讯云接口端点(可选)",
"workflow_node.deploy.form.tencentcloud_gaap_endpoint.placeholder": "请输入腾讯云 GAAP 接口端点(例如:gaap.tencentcloudapi.com",
"workflow_node.deploy.form.tencentcloud_gaap_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/608/36934\" target=\"_blank\">https://cloud.tencent.com/document/product/608/36934</a><br>国际站用户请填写 <em>gaap.intl.tencentcloudapi.com</em>。",