diff --git a/internal/certdeploy/deployers/sp_tencentcloud_css.go b/internal/certdeploy/deployers/sp_tencentcloud_css.go index bb976ca2..eb728d70 100644 --- a/internal/certdeploy/deployers/sp_tencentcloud_css.go +++ b/internal/certdeploy/deployers/sp_tencentcloud_css.go @@ -17,10 +17,11 @@ func init() { } provider, err := tencentcloudcss.NewSSLDeployerProvider(&tencentcloudcss.SSLDeployerProviderConfig{ - SecretId: credentials.SecretId, - SecretKey: credentials.SecretKey, - Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), - Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), + SecretId: credentials.SecretId, + SecretKey: credentials.SecretKey, + Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), + DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), + Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) diff --git a/pkg/core/ssl-deployer/providers/jdcloud-vod/jdcloud_vod.go b/pkg/core/ssl-deployer/providers/jdcloud-vod/jdcloud_vod.go index 97f4b9ab..3e285972 100644 --- a/pkg/core/ssl-deployer/providers/jdcloud-vod/jdcloud_vod.go +++ b/pkg/core/ssl-deployer/providers/jdcloud-vod/jdcloud_vod.go @@ -130,7 +130,7 @@ func (d *SSLDeployerProvider) getAllDomains(ctx context.Context) ([]string, erro // 查询域名列表 // REF: https://docs.jdcloud.com/cn/video-on-demand/api/listdomains - listPageNumber := 1 + listDomainsPageNumber := 1 listDomainsPageSize := 100 for { select { @@ -140,7 +140,7 @@ func (d *SSLDeployerProvider) getAllDomains(ctx context.Context) ([]string, erro } listDomainsReq := jdvod.NewListDomainsRequestWithoutParam() - listDomainsReq.SetPageNumber(listPageNumber) + listDomainsReq.SetPageNumber(listDomainsPageNumber) listDomainsReq.SetPageSize(listDomainsPageSize) listDomainsResp, err := d.sdkClient.ListDomains(listDomainsReq) d.logger.Debug("sdk request 'vod.ListDomains'", slog.Any("request", listDomainsReq), slog.Any("response", listDomainsResp)) @@ -161,7 +161,7 @@ func (d *SSLDeployerProvider) getAllDomains(ctx context.Context) ([]string, erro break } - listPageNumber++ + listDomainsPageNumber++ } return domains, nil diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-css/consts.go b/pkg/core/ssl-deployer/providers/tencentcloud-css/consts.go new file mode 100644 index 00000000..add86a6a --- /dev/null +++ b/pkg/core/ssl-deployer/providers/tencentcloud-css/consts.go @@ -0,0 +1,8 @@ +package tencentcloudcss + +const ( + // 匹配模式:精确匹配。 + DOMAIN_MATCH_PATTERN_EXACT = "exact" + // 匹配模式:证书 SAN 匹配。 + DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" +) diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-css/internal/client.go b/pkg/core/ssl-deployer/providers/tencentcloud-css/internal/client.go index 621dfc23..1b7aefad 100644 --- a/pkg/core/ssl-deployer/providers/tencentcloud-css/internal/client.go +++ b/pkg/core/ssl-deployer/providers/tencentcloud-css/internal/client.go @@ -23,6 +23,27 @@ func NewLiveClient(credential common.CredentialIface, region string, clientProfi return } +func (c *LiveClient) DescribeLiveDomains(request *tclive.DescribeLiveDomainsRequest) (response *tclive.DescribeLiveDomainsResponse, err error) { + return c.DescribeLiveDomainsWithContext(context.Background(), request) +} + +func (c *LiveClient) DescribeLiveDomainsWithContext(ctx context.Context, request *tclive.DescribeLiveDomainsRequest) (response *tclive.DescribeLiveDomainsResponse, err error) { + if request == nil { + request = tclive.NewDescribeLiveDomainsRequest() + } + c.InitBaseRequest(&request.BaseRequest, "live", tclive.APIVersion, "DescribeLiveDomains") + + if c.GetCredential() == nil { + return nil, errors.New("DescribeLiveDomains require credential") + } + + request.SetContext(ctx) + + response = tclive.NewDescribeLiveDomainsResponse() + err = c.Send(request, response) + return +} + func (c *LiveClient) ModifyLiveDomainCertBindings(request *tclive.ModifyLiveDomainCertBindingsRequest) (response *tclive.ModifyLiveDomainCertBindingsResponse, err error) { return c.ModifyLiveDomainCertBindingsWithContext(context.Background(), request) } diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-css/tencentcloud_css.go b/pkg/core/ssl-deployer/providers/tencentcloud-css/tencentcloud_css.go index f29ba15d..b8602636 100644 --- a/pkg/core/ssl-deployer/providers/tencentcloud-css/tencentcloud_css.go +++ b/pkg/core/ssl-deployer/providers/tencentcloud-css/tencentcloud_css.go @@ -15,6 +15,7 @@ import ( "github.com/certimate-go/certimate/pkg/core" "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/tencentcloud-css/internal" sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/tencentcloud-ssl" + xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type SSLDeployerProviderConfig struct { @@ -24,6 +25,9 @@ type SSLDeployerProviderConfig struct { SecretKey string `json:"secretKey"` // 腾讯云接口端点。 Endpoint string `json:"endpoint,omitempty"` + // 域名匹配模式。 + // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 + DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 直播播放域名(不支持泛域名)。 Domain string `json:"domain"` } @@ -77,10 +81,6 @@ func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) { } func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) { - if d.config.Domain == "" { - return nil, errors.New("config `domain` is required") - } - // 上传证书 upres, err := d.sslManager.Upload(ctx, certPEM, privkeyPEM) if err != nil { @@ -89,15 +89,51 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } - // 绑定证书对应的播放域名 + // 获取待部署的域名列表 + var domains []string + switch d.config.DomainMatchPattern { + case "", DOMAIN_MATCH_PATTERN_EXACT: + { + if d.config.Domain == "" { + return nil, errors.New("config `domain` is required") + } + + domains = []string{d.config.Domain} + } + + case DOMAIN_MATCH_PATTERN_CERTSAN: + { + certX509, err := xcert.ParseCertificateFromPEM(certPEM) + if err != nil { + return nil, err + } + + domainCandidates, err := d.getAllDomains(ctx) + if err != nil { + return nil, err + } + + domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { + return certX509.VerifyHostname(domain) == nil + }) + if len(domains) == 0 { + return nil, errors.New("could not find any domains matched by certificate") + } + } + + default: + return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) + } + + // 批量绑定证书对应的播放域名 // REF: https://cloud.tencent.com/document/api/267/78655 modifyLiveDomainCertBindingsReq := tclive.NewModifyLiveDomainCertBindingsRequest() - modifyLiveDomainCertBindingsReq.DomainInfos = []*tclive.LiveCertDomainInfo{ - { - DomainName: common.StringPtr(d.config.Domain), + modifyLiveDomainCertBindingsReq.DomainInfos = lo.Map(domains, func(domain string, _ int) *tclive.LiveCertDomainInfo { + return &tclive.LiveCertDomainInfo{ + DomainName: common.StringPtr(domain), Status: common.Int64Ptr(1), - }, - } + } + }) modifyLiveDomainCertBindingsReq.CloudCertId = common.StringPtr(upres.CertId) modifyLiveDomainCertBindingsResp, err := d.sdkClient.ModifyLiveDomainCertBindings(modifyLiveDomainCertBindingsReq) d.logger.Debug("sdk request 'live.ModifyLiveDomainCertBindings'", slog.Any("request", modifyLiveDomainCertBindingsReq), slog.Any("response", modifyLiveDomainCertBindingsResp)) @@ -108,6 +144,49 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke return &core.SSLDeployResult{}, nil } +func (d *SSLDeployerProvider) getAllDomains(ctx context.Context) ([]string, error) { + domains := make([]string, 0) + + // 查询域名列表 + // REF: https://cloud.tencent.com/document/api/267/33856 + describeLiveDomainsPageNum := 1 + describeLiveDomainsPageSize := 100 + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + describeLiveDomainsReq := tclive.NewDescribeLiveDomainsRequest() + describeLiveDomainsReq.DomainStatus = common.Uint64Ptr(1) + describeLiveDomainsReq.DomainType = common.Uint64Ptr(1) + describeLiveDomainsReq.PageNum = common.Uint64Ptr(uint64(describeLiveDomainsPageNum)) + describeLiveDomainsReq.PageSize = common.Uint64Ptr(uint64(describeLiveDomainsPageSize)) + describeLiveDomainsResp, err := d.sdkClient.DescribeLiveDomains(describeLiveDomainsReq) + d.logger.Debug("sdk request 'live.DescribeLiveDomains'", slog.Any("request", describeLiveDomainsReq), slog.Any("response", describeLiveDomainsResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'live.DescribeLiveDomains': %w", err) + } + + if describeLiveDomainsResp.Response == nil { + break + } + + for _, domainItem := range describeLiveDomainsResp.Response.DomainList { + domains = append(domains, *domainItem.Name) + } + + if len(describeLiveDomainsResp.Response.DomainList) < describeLiveDomainsPageSize { + break + } + + describeLiveDomainsPageNum++ + } + + return domains, nil +} + func createSDKClient(secretId, secretKey, endpoint string) (*internal.LiveClient, error) { credential := common.NewCredential(secretId, secretKey) diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCSS.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCSS.tsx index b0559d7b..d6793eb4 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCSS.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCSS.tsx @@ -1,12 +1,16 @@ import { getI18n, useTranslation } from "react-i18next"; -import { Form, Input } from "antd"; +import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import Show from "@/components/Show"; import { validDomainName } from "@/utils/validators"; import { useFormNestedFieldsContext } from "./_context"; +const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; +const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; + const BizDeployNodeConfigFieldsProviderTencentCloudCSS = () => { const { i18n, t } = useTranslation(); @@ -15,8 +19,11 @@ const BizDeployNodeConfigFieldsProviderTencentCloudCSS = () => { [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); + const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); + const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); + return ( <> { - + ({ + key: s, + label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), + value: s, + }))} + /> + + + + + + ); }; const getInitialValues = (): Nullish>> => { return { + domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; @@ -50,10 +75,29 @@ const getInitialValues = (): Nullish>> => { const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; - return z.object({ - endpoint: z.string().nullish(), - domain: z.string().refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); + return z + .object({ + endpoint: z.string().nullish(), + domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), + domain: z.string().nullish(), + }) + .superRefine((values, ctx) => { + if (values.domainMatchPattern) { + switch (values.domainMatchPattern) { + case DOMAIN_MATCH_PATTERN_EXACT: + { + if (!validDomainName(values.domain!)) { + ctx.addIssue({ + code: "custom", + message: t("common.errmsg.domain_invalid"), + path: ["domain"], + }); + } + } + break; + } + } + }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudCSS, {