From d0a62215df2455300dd6b8a46343fc464cbcd853 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 jdcloud vod --- .../certdeploy/deployers/sp_jdcloud_vod.go | 7 +- .../providers/jdcloud-vod/consts.go | 8 ++ .../providers/jdcloud-vod/jdcloud_vod.go | 120 +++++++++++++++++- .../providers/jdcloud-vod/jdcloud_vod_test.go | 7 +- ...ployNodeConfigFieldsProviderJDCloudVOD.tsx | 60 +++++++-- 5 files changed, 183 insertions(+), 19 deletions(-) create mode 100644 pkg/core/ssl-deployer/providers/jdcloud-vod/consts.go diff --git a/internal/certdeploy/deployers/sp_jdcloud_vod.go b/internal/certdeploy/deployers/sp_jdcloud_vod.go index 0c39162b..9070e14e 100644 --- a/internal/certdeploy/deployers/sp_jdcloud_vod.go +++ b/internal/certdeploy/deployers/sp_jdcloud_vod.go @@ -17,9 +17,10 @@ func init() { } provider, err := jdcloudvod.NewSSLDeployerProvider(&jdcloudvod.SSLDeployerProviderConfig{ - AccessKeyId: credentials.AccessKeyId, - AccessKeySecret: credentials.AccessKeySecret, - Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), + AccessKeyId: credentials.AccessKeyId, + AccessKeySecret: credentials.AccessKeySecret, + 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/consts.go b/pkg/core/ssl-deployer/providers/jdcloud-vod/consts.go new file mode 100644 index 00000000..2ccb6044 --- /dev/null +++ b/pkg/core/ssl-deployer/providers/jdcloud-vod/consts.go @@ -0,0 +1,8 @@ +package jdcloudvod + +const ( + // 匹配模式:精确匹配。 + DOMAIN_MATCH_PATTERN_EXACT = "exact" + // 匹配模式:证书 SAN 匹配。 + DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" +) 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 5bb07872..97f4b9ab 100644 --- a/pkg/core/ssl-deployer/providers/jdcloud-vod/jdcloud_vod.go +++ b/pkg/core/ssl-deployer/providers/jdcloud-vod/jdcloud_vod.go @@ -11,8 +11,10 @@ import ( "github.com/certimate-go/certimate/pkg/core" jdcore "github.com/jdcloud-api/jdcloud-sdk-go/core" jdvod "github.com/jdcloud-api/jdcloud-sdk-go/services/vod/apis" + "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/jdcloud-vod/internal" + xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type SSLDeployerProviderConfig struct { @@ -20,6 +22,9 @@ type SSLDeployerProviderConfig struct { AccessKeyId string `json:"accessKeyId"` // 京东云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 域名匹配模式。 + // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 + DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 点播加速域名(不支持泛域名)。 Domain string `json:"domain"` } @@ -58,10 +63,115 @@ func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) { } func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) { + // 获取待部署的域名列表 + 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 vod domains to deploy") + } else { + d.logger.Info("found vod 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, certPEM, privkeyPEM); 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://docs.jdcloud.com/cn/video-on-demand/api/listdomains + listPageNumber := 1 + listDomainsPageSize := 100 + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + listDomainsReq := jdvod.NewListDomainsRequestWithoutParam() + listDomainsReq.SetPageNumber(listPageNumber) + listDomainsReq.SetPageSize(listDomainsPageSize) + listDomainsResp, err := d.sdkClient.ListDomains(listDomainsReq) + d.logger.Debug("sdk request 'vod.ListDomains'", slog.Any("request", listDomainsReq), slog.Any("response", listDomainsResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'vod.ListDomains': %w", err) + } + + ignoredStatuses := []string{"init", "stopped"} + for _, domainItem := range listDomainsResp.Result.Content { + if lo.Contains(ignoredStatuses, domainItem.Status) { + continue + } + + domains = append(domains, domainItem.Name) + } + + if len(listDomainsResp.Result.Content) < listDomainsPageSize { + break + } + + listPageNumber++ + } + + return domains, nil +} + +func (d *SSLDeployerProvider) updateDomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error { // 获取域名 ID - domainId, err := d.findDomainIdByDomain(ctx, d.config.Domain) + domainId, err := d.findDomainIdByDomain(ctx, domain) if err != nil { - return nil, err + return err } // 查询域名 SSL 配置 @@ -71,7 +181,7 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke getHttpSslResp, err := d.sdkClient.GetHttpSsl(getHttpSslReq) d.logger.Debug("sdk request 'vod.GetHttpSsl'", slog.Any("request", getHttpSslReq), slog.Any("response", getHttpSslResp)) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'vod.GetHttpSsl': %w", err) + return fmt.Errorf("failed to execute sdk request 'vod.GetHttpSsl': %w", err) } // 设置域名 SSL 配置 @@ -87,10 +197,10 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke setHttpSslResp, err := d.sdkClient.SetHttpSsl(setHttpSslReq) d.logger.Debug("sdk request 'vod.SetHttpSsl'", slog.Any("request", setHttpSslReq), slog.Any("response", setHttpSslResp)) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'vod.SetHttpSsl': %w", err) + return fmt.Errorf("failed to execute sdk request 'vod.SetHttpSsl': %w", err) } - return &core.SSLDeployResult{}, nil + return nil } func (d *SSLDeployerProvider) findDomainIdByDomain(ctx context.Context, domain string) (int, error) { diff --git a/pkg/core/ssl-deployer/providers/jdcloud-vod/jdcloud_vod_test.go b/pkg/core/ssl-deployer/providers/jdcloud-vod/jdcloud_vod_test.go index c0993320..dd910d7a 100644 --- a/pkg/core/ssl-deployer/providers/jdcloud-vod/jdcloud_vod_test.go +++ b/pkg/core/ssl-deployer/providers/jdcloud-vod/jdcloud_vod_test.go @@ -53,9 +53,10 @@ func TestDeploy(t *testing.T) { }, "\n")) deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{ - AccessKeyId: fAccessKeyId, - AccessKeySecret: fAccessKeySecret, - Domain: fDomain, + AccessKeyId: fAccessKeyId, + AccessKeySecret: fAccessKeySecret, + 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/BizDeployNodeConfigFieldsProviderJDCloudVOD.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderJDCloudVOD.tsx index 7577093b..38e56455 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderJDCloudVOD.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderJDCloudVOD.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 BizDeployNodeConfigFieldsProviderJDCloudVOD = () => { const { i18n, t } = useTranslation(); @@ -15,24 +19,45 @@ const BizDeployNodeConfigFieldsProviderJDCloudVOD = () => { [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: "", }; }; @@ -40,9 +65,28 @@ const getInitialValues = (): Nullish>> => { const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; - return z.object({ - domain: z.string().refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), - }); + return z + .object({ + 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(BizDeployNodeConfigFieldsProviderJDCloudVOD, {