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 ( <>