From a91b2f0cec481e2c103dfbb475f13fb4852d4e0e Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 13 Nov 2025 23:00:21 +0800 Subject: [PATCH] feat(provider): supoprt certsan match pattern in deployment to tencentcloud scf --- .../deployers/sp_tencentcloud_scf.go | 11 +- .../providers/tencentcloud-scf/consts.go | 8 + .../tencentcloud-scf/internal/client.go | 21 +++ .../tencentcloud-scf/tencentcloud_scf.go | 145 ++++++++++++++++-- .../tencentcloud-scf/tencentcloud_scf_test.go | 9 +- ...odeConfigFieldsProviderTencentCloudSCF.tsx | 64 ++++++-- 6 files changed, 222 insertions(+), 36 deletions(-) create mode 100644 pkg/core/ssl-deployer/providers/tencentcloud-scf/consts.go diff --git a/internal/certdeploy/deployers/sp_tencentcloud_scf.go b/internal/certdeploy/deployers/sp_tencentcloud_scf.go index 4a85c436..f28b1dbc 100644 --- a/internal/certdeploy/deployers/sp_tencentcloud_scf.go +++ b/internal/certdeploy/deployers/sp_tencentcloud_scf.go @@ -17,11 +17,12 @@ func init() { } provider, err := tencentcloudscf.NewSSLDeployerProvider(&tencentcloudscf.SSLDeployerProviderConfig{ - SecretId: credentials.SecretId, - SecretKey: credentials.SecretKey, - Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), - Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), - Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), + SecretId: credentials.SecretId, + SecretKey: credentials.SecretKey, + Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), + Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), + DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), + Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-scf/consts.go b/pkg/core/ssl-deployer/providers/tencentcloud-scf/consts.go new file mode 100644 index 00000000..853d0a31 --- /dev/null +++ b/pkg/core/ssl-deployer/providers/tencentcloud-scf/consts.go @@ -0,0 +1,8 @@ +package tencentcloudscf + +const ( + // 匹配模式:精确匹配。 + DOMAIN_MATCH_PATTERN_EXACT = "exact" + // 匹配模式:证书 SAN 匹配。 + DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" +) diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-scf/internal/client.go b/pkg/core/ssl-deployer/providers/tencentcloud-scf/internal/client.go index cf166189..3f7df8b1 100644 --- a/pkg/core/ssl-deployer/providers/tencentcloud-scf/internal/client.go +++ b/pkg/core/ssl-deployer/providers/tencentcloud-scf/internal/client.go @@ -43,6 +43,27 @@ func (c *ScfClient) GetCustomDomainWithContext(ctx context.Context, request *tcs return } +func (c *ScfClient) ListCustomDomains(request *tcscf.ListCustomDomainsRequest) (response *tcscf.ListCustomDomainsResponse, err error) { + return c.ListCustomDomainsWithContext(context.Background(), request) +} + +func (c *ScfClient) ListCustomDomainsWithContext(ctx context.Context, request *tcscf.ListCustomDomainsRequest) (response *tcscf.ListCustomDomainsResponse, err error) { + if request == nil { + request = tcscf.NewListCustomDomainsRequest() + } + c.InitBaseRequest(&request.BaseRequest, "scf", tcscf.APIVersion, "ListCustomDomains") + + if c.GetCredential() == nil { + return nil, errors.New("ListCustomDomains require credential") + } + + request.SetContext(ctx) + + response = tcscf.NewListCustomDomainsResponse() + err = c.Send(request, response) + return +} + func (c *ScfClient) UpdateCustomDomain(request *tcscf.UpdateCustomDomainRequest) (response *tcscf.UpdateCustomDomainResponse, err error) { return c.UpdateCustomDomainWithContext(context.Background(), request) } diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-scf/tencentcloud_scf.go b/pkg/core/ssl-deployer/providers/tencentcloud-scf/tencentcloud_scf.go index 6353ec8e..8fd1b508 100644 --- a/pkg/core/ssl-deployer/providers/tencentcloud-scf/tencentcloud_scf.go +++ b/pkg/core/ssl-deployer/providers/tencentcloud-scf/tencentcloud_scf.go @@ -15,6 +15,7 @@ import ( "github.com/certimate-go/certimate/pkg/core" "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/tencentcloud-scf/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 { @@ -26,6 +27,9 @@ type SSLDeployerProviderConfig struct { Endpoint string `json:"endpoint,omitempty"` // 腾讯云地域。 Region string `json:"region"` + // 域名匹配模式。 + // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 + DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 自定义域名(不支持泛域名)。 Domain string `json:"domain"` } @@ -79,20 +83,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") - } - - // 查看云函数自定义域名详情 - // REF: https://cloud.tencent.com/document/api/583/111924 - getCustomDomainReq := tcscf.NewGetCustomDomainRequest() - getCustomDomainReq.Domain = common.StringPtr(d.config.Domain) - getCustomDomainResp, err := d.sdkClient.GetCustomDomain(getCustomDomainReq) - d.logger.Debug("sdk request 'scf.GetCustomDomain'", slog.Any("request", getCustomDomainReq), slog.Any("response", getCustomDomainResp)) - if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'scf.GetCustomDomain': %w", err) - } - // 上传证书 upres, err := d.sslManager.Upload(ctx, certPEM, privkeyPEM) if err != nil { @@ -101,21 +91,142 @@ 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) + } + + // 遍历更新域名证书 + if len(domains) == 0 { + d.logger.Info("no scf domains to deploy") + } else { + d.logger.Info("found scf domains to deploy", slog.Any("domains", domains)) + var errs []error + + for _, domain := range domains { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + if err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil { + errs = append(errs, err) + } + } + } + + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + } + + return &core.SSLDeployResult{}, nil +} + +func (d *SSLDeployerProvider) getAllDomains(ctx context.Context) ([]string, error) { + domains := make([]string, 0) + + // 获取云函数自定义域名列表 + // REF: https://cloud.tencent.com/document/api/583/111923 + listCustomDomainsOffset := 0 + listCustomDomainsLimit := 20 + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + describeLiveDomainsReq := tcscf.NewListCustomDomainsRequest() + describeLiveDomainsReq.Offset = common.Uint64Ptr(uint64(listCustomDomainsOffset)) + describeLiveDomainsReq.Limit = common.Uint64Ptr(uint64(listCustomDomainsLimit)) + describeLiveDomainsResp, err := d.sdkClient.ListCustomDomains(describeLiveDomainsReq) + d.logger.Debug("sdk request 'scf.DescribeLiveDomains'", slog.Any("request", describeLiveDomainsReq), slog.Any("response", describeLiveDomainsResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'scf.DescribeLiveDomains': %w", err) + } + + if describeLiveDomainsResp.Response == nil { + break + } + + for _, domainItem := range describeLiveDomainsResp.Response.Domains { + domains = append(domains, *domainItem.Domain) + } + + if len(describeLiveDomainsResp.Response.Domains) < listCustomDomainsLimit { + break + } + + listCustomDomainsOffset++ + } + + return domains, nil +} + +func (d *SSLDeployerProvider) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error { + // 查看云函数自定义域名详情 + // REF: https://cloud.tencent.com/document/api/583/111924 + getCustomDomainReq := tcscf.NewGetCustomDomainRequest() + getCustomDomainReq.Domain = common.StringPtr(d.config.Domain) + getCustomDomainResp, err := d.sdkClient.GetCustomDomain(getCustomDomainReq) + d.logger.Debug("sdk request 'scf.GetCustomDomain'", slog.Any("request", getCustomDomainReq), slog.Any("response", getCustomDomainResp)) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'scf.GetCustomDomain': %w", err) + } else { + if getCustomDomainResp.Response.CertConfig != nil && getCustomDomainResp.Response.CertConfig.CertificateId != nil && *getCustomDomainResp.Response.CertConfig.CertificateId == cloudCertId { + return nil + } + } + // 更新云函数自定义域名 // REF: https://cloud.tencent.com/document/api/583/111922 updateCustomDomainReq := tcscf.NewUpdateCustomDomainRequest() updateCustomDomainReq.Domain = common.StringPtr(d.config.Domain) updateCustomDomainReq.CertConfig = &tcscf.CertConf{ - CertificateId: common.StringPtr(upres.CertId), + CertificateId: common.StringPtr(cloudCertId), } updateCustomDomainReq.Protocol = getCustomDomainResp.Response.Protocol + if updateCustomDomainReq.Protocol == nil || *updateCustomDomainReq.Protocol == "HTTP" { + updateCustomDomainReq.Protocol = common.StringPtr("HTTP&HTTPS") + } updateCustomDomainResp, err := d.sdkClient.UpdateCustomDomain(updateCustomDomainReq) d.logger.Debug("sdk request 'scf.UpdateCustomDomain'", slog.Any("request", updateCustomDomainReq), slog.Any("response", updateCustomDomainResp)) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'scf.UpdateCustomDomain': %w", err) + return fmt.Errorf("failed to execute sdk request 'scf.UpdateCustomDomain': %w", err) } - return &core.SSLDeployResult{}, nil + return nil } func createSDKClient(secretId, secretKey, endpoint, region string) (*internal.ScfClient, error) { diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-scf/tencentcloud_scf_test.go b/pkg/core/ssl-deployer/providers/tencentcloud-scf/tencentcloud_scf_test.go index 0357028d..de654e37 100644 --- a/pkg/core/ssl-deployer/providers/tencentcloud-scf/tencentcloud_scf_test.go +++ b/pkg/core/ssl-deployer/providers/tencentcloud-scf/tencentcloud_scf_test.go @@ -57,10 +57,11 @@ func TestDeploy(t *testing.T) { }, "\n")) deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{ - SecretId: fSecretId, - SecretKey: fSecretKey, - Region: fRegion, - Domain: fDomain, + SecretId: fSecretId, + SecretKey: fSecretKey, + Region: fRegion, + DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, + Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudSCF.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudSCF.tsx index b9ec5901..82396383 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudSCF.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudSCF.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 BizDeployNodeConfigFieldsProviderTencentCloudSCF = () => { const { i18n, t } = useTranslation(); @@ -15,8 +19,11 @@ const BizDeployNodeConfigFieldsProviderTencentCloudSCF = () => { [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, + }))} + /> + + + + + + ); }; @@ -54,6 +78,7 @@ const BizDeployNodeConfigFieldsProviderTencentCloudSCF = () => { const getInitialValues = (): Nullish>> => { return { region: "", + domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; @@ -61,11 +86,30 @@ const getInitialValues = (): Nullish>> => { const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; - return z.object({ - endpoint: z.string().nullish(), - region: z.string().nonempty(t("workflow_node.deploy.form.tencentcloud_scf_region.placeholder")), - domain: z.string().refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); + return z + .object({ + endpoint: z.string().nullish(), + region: z.string().nonempty(t("workflow_node.deploy.form.tencentcloud_scf_region.placeholder")), + 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(BizDeployNodeConfigFieldsProviderTencentCloudSCF, {