diff --git a/internal/certdeploy/deployers/sp_tencentcloud_cdn.go b/internal/certdeploy/deployers/sp_tencentcloud_cdn.go index 343d78c06..6033c06d3 100644 --- a/internal/certdeploy/deployers/sp_tencentcloud_cdn.go +++ b/internal/certdeploy/deployers/sp_tencentcloud_cdn.go @@ -17,10 +17,11 @@ func init() { } provider, err := tencentcloudcdn.NewSSLDeployerProvider(&tencentcloudcdn.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"), + MatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "matchPattern"), + Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }); err != nil { diff --git a/internal/certdeploy/deployers/sp_tencentcloud_ecdn.go b/internal/certdeploy/deployers/sp_tencentcloud_ecdn.go index 8993578ec..5961e1362 100644 --- a/internal/certdeploy/deployers/sp_tencentcloud_ecdn.go +++ b/internal/certdeploy/deployers/sp_tencentcloud_ecdn.go @@ -17,10 +17,11 @@ func init() { } provider, err := tencentcloudecdn.NewSSLDeployerProvider(&tencentcloudecdn.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"), + MatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "matchPattern"), + Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }); err != nil { diff --git a/internal/certdeploy/deployers/sp_volcengine_cdn.go b/internal/certdeploy/deployers/sp_volcengine_cdn.go index 8553338f2..b0bbaca49 100644 --- a/internal/certdeploy/deployers/sp_volcengine_cdn.go +++ b/internal/certdeploy/deployers/sp_volcengine_cdn.go @@ -19,6 +19,7 @@ func init() { provider, err := volcenginecdn.NewSSLDeployerProvider(&volcenginecdn.SSLDeployerProviderConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.SecretAccessKey, + MatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "matchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err diff --git a/internal/certdeploy/deployers/sp_volcengine_live.go b/internal/certdeploy/deployers/sp_volcengine_live.go index 95bfdbd61..d09787e7f 100644 --- a/internal/certdeploy/deployers/sp_volcengine_live.go +++ b/internal/certdeploy/deployers/sp_volcengine_live.go @@ -19,6 +19,7 @@ func init() { provider, err := volcenginelive.NewSSLDeployerProvider(&volcenginelive.SSLDeployerProviderConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.SecretAccessKey, + MatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "matchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err diff --git a/internal/workflow/engine/executor_bizmonitor.go b/internal/workflow/engine/executor_bizmonitor.go index b3304e712..1d8f40515 100644 --- a/internal/workflow/engine/executor_bizmonitor.go +++ b/internal/workflow/engine/executor_bizmonitor.go @@ -85,10 +85,7 @@ func (ne *bizMonitorNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeE now := time.Now() isCertPeriodValid := now.Before(cert.NotAfter) && now.After(cert.NotBefore) - isCertHostMatched := true - if err := cert.VerifyHostname(targetDomain); err != nil { - isCertHostMatched = false - } + isCertHostMatched := cert.VerifyHostname(targetDomain) == nil validated := isCertPeriodValid && isCertHostMatched daysLeft := int(math.Floor(time.Until(cert.NotAfter).Hours() / 24)) diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-cdn/consts.go b/pkg/core/ssl-deployer/providers/tencentcloud-cdn/consts.go new file mode 100644 index 000000000..50ef02ff8 --- /dev/null +++ b/pkg/core/ssl-deployer/providers/tencentcloud-cdn/consts.go @@ -0,0 +1,10 @@ +package tencentcloudcdn + +const ( + // 匹配模式:精确匹配。 + MatchPatternExact = "exact" + // 匹配模式:通配符匹配。 + MatchPatternWildcard = "wildcard" + // 匹配模式:证书 SAN 匹配。 + MatchPatternCertSAN = "certsan" +) diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go b/pkg/core/ssl-deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go index 9af49f98a..c04c5d3aa 100644 --- a/pkg/core/ssl-deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go +++ b/pkg/core/ssl-deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go @@ -15,6 +15,7 @@ import ( "github.com/certimate-go/certimate/pkg/core" 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"` + // 域名匹配模式。 + // 零值时默认值 [MatchPatternExact]。 + MatchPattern string `json:"matchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } @@ -95,17 +99,39 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke } // 获取待部署的 CDN 实例 - // 如果是泛域名,根据证书匹配 CDN 实例 domains := make([]string, 0) - if strings.HasPrefix(d.config.Domain, "*.") { - temp, err := d.getDomainsByCertId(ctx, upres.CertId) - if err != nil { - return nil, err + switch d.config.MatchPattern { + case "", MatchPatternExact: + { + domains = append(domains, d.config.Domain) } - domains = temp - } else { - domains = append(domains, d.config.Domain) + case MatchPatternWildcard: + { + if strings.HasPrefix(d.config.Domain, "*.") { + temp, err := d.getMatchedDomainsByWildcard(ctx, d.config.Domain) + if err != nil { + return nil, err + } + + domains = temp + } else { + domains = append(domains, d.config.Domain) + } + } + + case MatchPatternCertSAN: + { + temp, err := d.getMatchedDomainsByCertId(ctx, upres.CertId) + if err != nil { + return nil, err + } + + domains = temp + } + + default: + return nil, fmt.Errorf("unsupported match pattern: '%s'", d.config.MatchPattern) } // 遍历更新域名证书 @@ -134,7 +160,55 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke return &core.SSLDeployResult{}, nil } -func (d *SSLDeployerProvider) getDomainsByCertId(ctx context.Context, cloudCertId string) ([]string, error) { +func (d *SSLDeployerProvider) getMatchedDomainsByWildcard(ctx context.Context, wildcardDomain string) ([]string, error) { + domains := make([]string, 0) + + // 遍历查询域名基本信息,获取匹配的域名 + // REF: https://cloud.tencent.com/document/api/228/41118 + describeDomainsOffset := int64(0) + describeDomainsLimit := int64(100) + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + describeDomainsReq := tccdn.NewDescribeDomainsRequest() + describeDomainsReq.Filters = []*tccdn.DomainFilter{ + { + Name: common.StringPtr("domain"), + Value: common.StringPtrs([]string{strings.TrimPrefix(wildcardDomain, "*.")}), + Fuzzy: common.BoolPtr(true), + }, + } + describeDomainsReq.Offset = common.Int64Ptr(describeDomainsOffset) + describeDomainsReq.Limit = common.Int64Ptr(describeDomainsLimit) + describeDomainsResp, err := d.sdkClient.DescribeDomains(describeDomainsReq) + d.logger.Debug("sdk request 'cdn.DescribeDomains'", slog.Any("request", describeDomainsReq), slog.Any("response", describeDomainsResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'cdn.DescribeDomains': %w", err) + } + + if describeDomainsResp.Response.Domains != nil { + for _, domain := range describeDomainsResp.Response.Domains { + if lo.FromPtr(domain.Product) == "cdn" && xcert.MatchHostname(wildcardDomain, lo.FromPtr(domain.Domain)) { + domains = append(domains, *domain.Domain) + } + } + } + + if len(describeDomainsResp.Response.Domains) < int(describeDomainsLimit) { + break + } else { + describeDomainsOffset += describeDomainsLimit + } + } + + return domains, nil +} + +func (d *SSLDeployerProvider) getMatchedDomainsByCertId(ctx context.Context, cloudCertId string) ([]string, error) { // 获取证书中的可用域名 // REF: https://cloud.tencent.com/document/api/228/42491 describeCertDomainsReq := tccdn.NewDescribeCertDomainsRequest() @@ -177,7 +251,10 @@ func (d *SSLDeployerProvider) updateDomainHttpsServerCert(ctx context.Context, d } domainConfig := describeDomainsConfigResp.Response.Domains[0] - if domainConfig.Https != nil && domainConfig.Https.CertInfo != nil && domainConfig.Https.CertInfo.CertId != nil && *domainConfig.Https.CertInfo.CertId == cloudCertId { + if domainConfig.Https != nil && + domainConfig.Https.CertInfo != nil && + domainConfig.Https.CertInfo.CertId != nil && + *domainConfig.Https.CertInfo.CertId == cloudCertId { // 已部署过此域名,跳过 return nil } @@ -188,9 +265,7 @@ func (d *SSLDeployerProvider) updateDomainHttpsServerCert(ctx context.Context, d updateDomainConfigReq.Domain = common.StringPtr(domain) updateDomainConfigReq.Https = domainConfig.Https if updateDomainConfigReq.Https == nil { - updateDomainConfigReq.Https = &tccdn.Https{ - Switch: common.StringPtr("on"), - } + updateDomainConfigReq.Https = &tccdn.Https{Switch: common.StringPtr("on")} } else { updateDomainConfigReq.Https.SslStatus = nil } diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-ecdn/consts.go b/pkg/core/ssl-deployer/providers/tencentcloud-ecdn/consts.go new file mode 100644 index 000000000..0fc19885f --- /dev/null +++ b/pkg/core/ssl-deployer/providers/tencentcloud-ecdn/consts.go @@ -0,0 +1,10 @@ +package tencentcloudecdn + +const ( + // 匹配模式:精确匹配。 + MatchPatternExact = "exact" + // 匹配模式:通配符匹配。 + MatchPatternWildcard = "wildcard" + // 匹配模式:证书 SAN 匹配。 + MatchPatternCertSAN = "certsan" +) diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go b/pkg/core/ssl-deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go index 29b0ce0e2..11e51ab42 100644 --- a/pkg/core/ssl-deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go +++ b/pkg/core/ssl-deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go @@ -15,6 +15,7 @@ import ( "github.com/certimate-go/certimate/pkg/core" 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"` + // 域名匹配模式。 + // 零值时默认值 [MatchPatternExact]。 + MatchPattern string `json:"matchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } @@ -94,18 +98,40 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } - // 获取待部署的 CDN 实例 - // 如果是泛域名,根据证书匹配 CDN 实例 + // 获取待部署的 ECDN 实例 domains := make([]string, 0) - if strings.HasPrefix(d.config.Domain, "*.") { - temp, err := d.getDomainsByCertId(upres.CertId) - if err != nil { - return nil, err + switch d.config.MatchPattern { + case "", MatchPatternExact: + { + domains = append(domains, d.config.Domain) } - domains = temp - } else { - domains = append(domains, d.config.Domain) + case MatchPatternWildcard: + { + if strings.HasPrefix(d.config.Domain, "*.") { + temp, err := d.getMatchedDomainsByWildcard(ctx, d.config.Domain) + if err != nil { + return nil, err + } + + domains = temp + } else { + domains = append(domains, d.config.Domain) + } + } + + case MatchPatternCertSAN: + { + temp, err := d.getMatchedDomainsByCertId(ctx, upres.CertId) + if err != nil { + return nil, err + } + + domains = temp + } + + default: + return nil, fmt.Errorf("unsupported match pattern: '%s'", d.config.MatchPattern) } // 遍历更新域名证书 @@ -134,7 +160,55 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke return &core.SSLDeployResult{}, nil } -func (d *SSLDeployerProvider) getDomainsByCertId(cloudCertId string) ([]string, error) { +func (d *SSLDeployerProvider) getMatchedDomainsByWildcard(ctx context.Context, wildcardDomain string) ([]string, error) { + domains := make([]string, 0) + + // 遍历查询域名基本信息,获取匹配的域名 + // REF: https://cloud.tencent.com/document/api/228/41118 + describeDomainsOffset := int64(0) + describeDomainsLimit := int64(100) + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + describeDomainsReq := tccdn.NewDescribeDomainsRequest() + describeDomainsReq.Filters = []*tccdn.DomainFilter{ + { + Name: common.StringPtr("domain"), + Value: common.StringPtrs([]string{strings.TrimPrefix(wildcardDomain, "*.")}), + Fuzzy: common.BoolPtr(true), + }, + } + describeDomainsReq.Offset = common.Int64Ptr(describeDomainsOffset) + describeDomainsReq.Limit = common.Int64Ptr(describeDomainsLimit) + describeDomainsResp, err := d.sdkClient.DescribeDomains(describeDomainsReq) + d.logger.Debug("sdk request 'cdn.DescribeDomains'", slog.Any("request", describeDomainsReq), slog.Any("response", describeDomainsResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'cdn.DescribeDomains': %w", err) + } + + if describeDomainsResp.Response.Domains != nil { + for _, domain := range describeDomainsResp.Response.Domains { + if lo.FromPtr(domain.Product) == "ecdn" && xcert.MatchHostname(wildcardDomain, lo.FromPtr(domain.Domain)) { + domains = append(domains, *domain.Domain) + } + } + } + + if len(describeDomainsResp.Response.Domains) < int(describeDomainsLimit) { + break + } else { + describeDomainsOffset += describeDomainsLimit + } + } + + return domains, nil +} + +func (d *SSLDeployerProvider) getMatchedDomainsByCertId(ctx context.Context, cloudCertId string) ([]string, error) { // 获取证书中的可用域名 // REF: https://cloud.tencent.com/document/api/228/42491 describeCertDomainsReq := tccdn.NewDescribeCertDomainsRequest() diff --git a/pkg/core/ssl-deployer/providers/volcengine-cdn/consts.go b/pkg/core/ssl-deployer/providers/volcengine-cdn/consts.go new file mode 100644 index 000000000..dce96568e --- /dev/null +++ b/pkg/core/ssl-deployer/providers/volcengine-cdn/consts.go @@ -0,0 +1,10 @@ +package volcenginecdn + +const ( + // 匹配模式:精确匹配。 + MatchPatternExact = "exact" + // 匹配模式:通配符匹配。 + MatchPatternWildcard = "wildcard" + // 匹配模式:证书 SAN 匹配。 + MatchPatternCertSAN = "certsan" +) diff --git a/pkg/core/ssl-deployer/providers/volcengine-cdn/volcengine_cdn.go b/pkg/core/ssl-deployer/providers/volcengine-cdn/volcengine_cdn.go index 8bb1eb647..7f4ce29ca 100644 --- a/pkg/core/ssl-deployer/providers/volcengine-cdn/volcengine_cdn.go +++ b/pkg/core/ssl-deployer/providers/volcengine-cdn/volcengine_cdn.go @@ -8,9 +8,11 @@ import ( "strings" vecdn "github.com/volcengine/volc-sdk-golang/service/cdn" + ve "github.com/volcengine/volcengine-go-sdk/volcengine" "github.com/certimate-go/certimate/pkg/core" sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/volcengine-cdn" + xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type SSLDeployerProviderConfig struct { @@ -18,6 +20,9 @@ type SSLDeployerProviderConfig struct { AccessKeyId string `json:"accessKeyId"` // 火山引擎 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 域名匹配模式。 + // 零值时默认值 [MatchPatternExact]。 + MatchPattern string `json:"matchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } @@ -79,44 +84,47 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } + // 获取待部署的 CDN 实例 domains := make([]string, 0) - if strings.HasPrefix(d.config.Domain, "*.") { - // 获取指定证书可关联的域名 - // REF: https://www.volcengine.com/docs/6454/125711 - describeCertConfigReq := &vecdn.DescribeCertConfigRequest{ - CertId: upres.CertId, - } - describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq) - d.logger.Debug("sdk request 'cdn.DescribeCertConfig'", slog.Any("request", describeCertConfigReq), slog.Any("response", describeCertConfigResp)) - if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'cdn.DescribeCertConfig': %w", err) + switch d.config.MatchPattern { + case "", MatchPatternExact: + { + domains = append(domains, d.config.Domain) } - if describeCertConfigResp.Result.CertNotConfig != nil { - for i := range describeCertConfigResp.Result.CertNotConfig { - domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain) - } - } + case MatchPatternWildcard: + { + if strings.HasPrefix(d.config.Domain, "*.") { + temp, err := d.getMatchedDomainsByWildcard(ctx, d.config.Domain) + if err != nil { + return nil, err + } - if describeCertConfigResp.Result.OtherCertConfig != nil { - for i := range describeCertConfigResp.Result.OtherCertConfig { - domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain) - } - } - - if len(domains) == 0 { - if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 { - // 所有可关联的域名都配置了该证书,跳过部署 - d.logger.Info("no domains to deploy") + domains = temp } else { - return nil, errors.New("domain not found") + domains = append(domains, d.config.Domain) } } - } else { - domains = append(domains, d.config.Domain) + + case MatchPatternCertSAN: + { + temp, err := d.getMatchedDomainsByCertId(ctx, upres.CertId) + if err != nil { + return nil, err + } + + domains = temp + } + + default: + return nil, fmt.Errorf("unsupported match pattern: '%s'", d.config.MatchPattern) } - if len(domains) > 0 { + // 遍历绑定证书 + if len(domains) == 0 { + d.logger.Info("no cdn domains to deploy") + } else { + d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { @@ -124,15 +132,7 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke case <-ctx.Done(): return nil, ctx.Err() default: - // 关联证书与加速域名 - // REF: https://www.volcengine.com/docs/6454/125712 - batchDeployCertReq := &vecdn.BatchDeployCertRequest{ - CertId: upres.CertId, - Domain: domain, - } - batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq) - d.logger.Debug("sdk request 'cdn.BatchDeployCert'", slog.Any("request", batchDeployCertReq), slog.Any("response", batchDeployCertResp)) - if err != nil { + if err := d.bindCert(ctx, domain, upres.CertId); err != nil { errs = append(errs, err) } } @@ -145,3 +145,98 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke return &core.SSLDeployResult{}, nil } + +func (d *SSLDeployerProvider) getMatchedDomainsByWildcard(ctx context.Context, wildcardDomain string) ([]string, error) { + domains := make([]string, 0) + + // 遍历获取加速域名列表,获取匹配的域名 + // REF: https://www.volcengine.com/docs/6454/75269 + listCdnDomainsPageNum := int64(1) + listCdnDomainsPageSize := int64(100) + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + listCdnDomainsReq := &vecdn.ListCdnDomainsRequest{ + Domain: ve.String(strings.TrimPrefix(wildcardDomain, "*.")), + Status: ve.String("online"), + PageNum: ve.Int64(listCdnDomainsPageNum), + PageSize: ve.Int64(listCdnDomainsPageSize), + } + listCdnDomainsResp, err := d.sdkClient.ListCdnDomains(listCdnDomainsReq) + d.logger.Debug("sdk request 'cdn.ListCdnDomains'", slog.Any("request", listCdnDomainsReq), slog.Any("response", listCdnDomainsResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'cdn.ListCdnDomains': %w", err) + } + + if listCdnDomainsResp.Result.Data != nil { + for _, domain := range listCdnDomainsResp.Result.Data { + if xcert.MatchHostname(wildcardDomain, domain.Domain) { + domains = append(domains, domain.Domain) + } + } + } + + if len(listCdnDomainsResp.Result.Data) < int(listCdnDomainsPageSize) { + break + } else { + listCdnDomainsPageSize++ + } + } + + return domains, nil +} + +func (d *SSLDeployerProvider) getMatchedDomainsByCertId(ctx context.Context, cloudCertId string) ([]string, error) { + domains := make([]string, 0) + + // 获取指定证书可关联的域名 + // REF: https://www.volcengine.com/docs/6454/125711 + describeCertConfigReq := &vecdn.DescribeCertConfigRequest{ + CertId: cloudCertId, + } + describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq) + d.logger.Debug("sdk request 'cdn.DescribeCertConfig'", slog.Any("request", describeCertConfigReq), slog.Any("response", describeCertConfigResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'cdn.DescribeCertConfig': %w", err) + } + + if describeCertConfigResp.Result.CertNotConfig != nil { + for i := range describeCertConfigResp.Result.CertNotConfig { + domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain) + } + } + + if describeCertConfigResp.Result.OtherCertConfig != nil { + for i := range describeCertConfigResp.Result.OtherCertConfig { + domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain) + } + } + + if len(domains) == 0 { + if len(describeCertConfigResp.Result.SpecifiedCertConfig) == 0 { + return nil, errors.New("domains not found") + } + } + + return domains, nil +} + +func (d *SSLDeployerProvider) bindCert(ctx context.Context, domain string, cloudCertId string) error { + // 关联证书与加速域名 + // REF: https://www.volcengine.com/docs/6454/125712 + batchDeployCertReq := &vecdn.BatchDeployCertRequest{ + CertId: cloudCertId, + Domain: domain, + } + batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq) + d.logger.Debug("sdk request 'cdn.BatchDeployCert'", slog.Any("request", batchDeployCertReq), slog.Any("response", batchDeployCertResp)) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/core/ssl-deployer/providers/volcengine-live/consts.go b/pkg/core/ssl-deployer/providers/volcengine-live/consts.go new file mode 100644 index 000000000..825027a51 --- /dev/null +++ b/pkg/core/ssl-deployer/providers/volcengine-live/consts.go @@ -0,0 +1,8 @@ +package volcenginelive + +const ( + // 匹配模式:精确匹配。 + MatchPatternExact = "exact" + // 匹配模式:通配符匹配。 + MatchPatternWildcard = "wildcard" +) diff --git a/pkg/core/ssl-deployer/providers/volcengine-live/volcengine_live.go b/pkg/core/ssl-deployer/providers/volcengine-live/volcengine_live.go index 1737b1fe8..47fdecc95 100644 --- a/pkg/core/ssl-deployer/providers/volcengine-live/volcengine_live.go +++ b/pkg/core/ssl-deployer/providers/volcengine-live/volcengine_live.go @@ -12,6 +12,7 @@ import ( "github.com/certimate-go/certimate/pkg/core" sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/volcengine-live" + xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type SSLDeployerProviderConfig struct { @@ -19,6 +20,9 @@ type SSLDeployerProviderConfig struct { AccessKeyId string `json:"accessKeyId"` // 火山引擎 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 域名匹配模式。 + // 零值时默认值 [MatchPatternExact]。 + MatchPattern string `json:"matchPattern,omitempty"` // 直播流域名(支持泛域名)。 Domain string `json:"domain"` } @@ -80,51 +84,37 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } + // 获取待部署的直播实例 domains := make([]string, 0) - if strings.HasPrefix(d.config.Domain, "*.") { - listDomainDetailPageNum := int32(1) - listDomainDetailPageSize := int32(1000) - listDomainDetailTotal := 0 - for { - // 查询域名列表 - // REF: https://www.volcengine.com/docs/6469/1186277#%E6%9F%A5%E8%AF%A2%E5%9F%9F%E5%90%8D%E5%88%97%E8%A1%A8 - listDomainDetailReq := &velive.ListDomainDetailBody{ - PageNum: listDomainDetailPageNum, - PageSize: listDomainDetailPageSize, - } - listDomainDetailResp, err := d.sdkClient.ListDomainDetail(ctx, listDomainDetailReq) - d.logger.Debug("sdk request 'live.ListDomainDetail'", slog.Any("request", listDomainDetailReq), slog.Any("response", listDomainDetailResp)) - if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'live.ListDomainDetail': %w", err) - } + switch d.config.MatchPattern { + case "", MatchPatternExact: + { + domains = append(domains, d.config.Domain) + } - if listDomainDetailResp.Result.DomainList != nil { - for _, item := range listDomainDetailResp.Result.DomainList { - // 仅匹配泛域名的下一级子域名 - wildcardDomain := strings.TrimPrefix(d.config.Domain, "*") - if strings.HasSuffix(item.Domain, wildcardDomain) && !strings.Contains(strings.TrimSuffix(item.Domain, wildcardDomain), ".") { - domains = append(domains, item.Domain) - } + case MatchPatternWildcard: + { + if strings.HasPrefix(d.config.Domain, "*.") { + temp, err := d.getMatchedDomainsByWildcard(ctx, d.config.Domain) + if err != nil { + return nil, err } - } - listDomainDetailLen := len(listDomainDetailResp.Result.DomainList) - if listDomainDetailLen < int(listDomainDetailPageSize) || int(listDomainDetailResp.Result.Total) <= listDomainDetailTotal+listDomainDetailLen { - break + domains = temp } else { - listDomainDetailPageNum++ - listDomainDetailTotal += listDomainDetailLen + domains = append(domains, d.config.Domain) } } - if len(domains) == 0 { - return nil, errors.New("domain not found") - } - } else { - domains = append(domains, d.config.Domain) + default: + return nil, fmt.Errorf("unsupported match pattern: '%s'", d.config.MatchPattern) } - if len(domains) > 0 { + // 遍历绑定证书 + if len(domains) == 0 { + d.logger.Info("no live domains to deploy") + } else { + d.logger.Info("found live domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { @@ -132,16 +122,7 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke case <-ctx.Done(): return nil, ctx.Err() default: - // 绑定证书 - // REF: https://www.volcengine.com/docs/6469/1186278#%E7%BB%91%E5%AE%9A%E8%AF%81%E4%B9%A6 - bindCertReq := &velive.BindCertBody{ - ChainID: upres.CertId, - Domain: domain, - HTTPS: ve.Bool(true), - } - bindCertResp, err := d.sdkClient.BindCert(ctx, bindCertReq) - d.logger.Debug("sdk request 'live.BindCert'", slog.Any("request", bindCertReq), slog.Any("response", bindCertResp)) - if err != nil { + if err := d.bindCert(ctx, domain, upres.CertId); err != nil { errs = append(errs, err) } } @@ -154,3 +135,67 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke return &core.SSLDeployResult{}, nil } + +func (d *SSLDeployerProvider) getMatchedDomainsByWildcard(ctx context.Context, wildcardDomain string) ([]string, error) { + domains := make([]string, 0) + + // 遍历查询域名列表,获取匹配的域名 + // REF: https://www.volcengine.com/docs/6469/1186277#%E6%9F%A5%E8%AF%A2%E5%9F%9F%E5%90%8D%E5%88%97%E8%A1%A8 + listDomainDetailPageNum := int32(1) + listDomainDetailPageSize := int32(1000) + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + listDomainDetailReq := &velive.ListDomainDetailBody{ + DomainStatusList: ve.Int32Slice([]int32{0}), + PageNum: listDomainDetailPageNum, + PageSize: listDomainDetailPageSize, + } + listDomainDetailResp, err := d.sdkClient.ListDomainDetail(ctx, listDomainDetailReq) + d.logger.Debug("sdk request 'live.ListDomainDetail'", slog.Any("request", listDomainDetailReq), slog.Any("response", listDomainDetailResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'live.ListDomainDetail': %w", err) + } + + if listDomainDetailResp.Result.DomainList != nil { + for _, domain := range listDomainDetailResp.Result.DomainList { + if xcert.MatchHostname(wildcardDomain, domain.Domain) { + domains = append(domains, domain.Domain) + } + } + } + + if len(listDomainDetailResp.Result.DomainList) < int(listDomainDetailPageSize) { + break + } else { + listDomainDetailPageNum++ + } + } + + if len(domains) == 0 { + return nil, errors.New("domain not found") + } + + return domains, nil +} + +func (d *SSLDeployerProvider) bindCert(ctx context.Context, domain string, cloudCertId string) error { + // 绑定证书 + // REF: https://www.volcengine.com/docs/6469/1186278#%E7%BB%91%E5%AE%9A%E8%AF%81%E4%B9%A6 + bindCertReq := &velive.BindCertBody{ + ChainID: cloudCertId, + Domain: domain, + HTTPS: ve.Bool(true), + } + bindCertResp, err := d.sdkClient.BindCert(ctx, bindCertReq) + d.logger.Debug("sdk request 'live.BindCert'", slog.Any("request", bindCertReq), slog.Any("response", bindCertResp)) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/utils/cert/hostname.go b/pkg/utils/cert/hostname.go new file mode 100644 index 000000000..af4ae5d9a --- /dev/null +++ b/pkg/utils/cert/hostname.go @@ -0,0 +1,33 @@ +package cert + +import ( + "crypto/x509" + "net" + "strings" +) + +// 检查目标主机名是否匹配待匹配主机名。 +// +// 入参: +// - match: 待匹配主机名。可以是泛域名,如 "*.example.com"。 +// - candidate: 目标主机名。如 "sub.example.com"。 +// +// 出参: +// - 是否匹配。 +func MatchHostname(match, candidate string) bool { + if match == "" || candidate == "" { + return false + } + + if !strings.Contains(match, "*") { + return strings.EqualFold(match, candidate) + } + + mockCert := &x509.Certificate{} + if ip := net.ParseIP(candidate); ip != nil { + mockCert.IPAddresses = []net.IP{ip} + } else { + mockCert.DNSNames = []string{match} + } + return mockCert.VerifyHostname(candidate) == nil +} diff --git a/pkg/utils/cert/hostname_test.go b/pkg/utils/cert/hostname_test.go new file mode 100644 index 000000000..85a2095c8 --- /dev/null +++ b/pkg/utils/cert/hostname_test.go @@ -0,0 +1,50 @@ +package cert_test + +import ( + "testing" + + xcert "github.com/certimate-go/certimate/pkg/utils/cert" +) + +func TestCertUtil_Hostname(t *testing.T) { + t.Run("MatchHostname", func(t *testing.T) { + testCases := []struct { + wildcard string + target string + expected bool + }{ + {"*.example.com", "sub.example.com", true}, + {"*.example.com", "sub.sub.example.com", false}, + {"*.example.com", "example.com", false}, + + {"*.*.example.com", "a.b.example.com", false}, + {"*.*.example.com", "a.example.com", false}, + {"*.*.example.com", "a.b.c.example.com", false}, + + {"example.com", "example.com", true}, + {"example.com", "wrong.com", false}, + + {"", "example.com", false}, + {"*.example.com", "", false}, + + {"*.sub.example.com", "a.sub.example.com", true}, + {"*.sub.example.com", "a.b.sub.example.com", false}, + {"*.sub.example.com", "sub.example.com", false}, + + {"*.Example.COM", "sub.example.com", true}, + {"*.EXAMPLE.COM", "SUB.EXAMPLE.COM", true}, + } + + for _, tc := range testCases { + result := xcert.MatchHostname(tc.wildcard, tc.target) + status := "✓" + pf := t.Logf + if result != tc.expected { + status = "✗" + pf = t.Errorf + } + + pf("%s Wildcard: %-20s Target: %-20s Expected: %-5v Got: %-5v\n", status, tc.wildcard, tc.target, tc.expected, result) + } + }) +} diff --git a/ui/src/components/access/forms/AccessConfigFieldsProvider1Panel.tsx b/ui/src/components/access/forms/AccessConfigFieldsProvider1Panel.tsx index 6441e14e3..725da3dd0 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProvider1Panel.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProvider1Panel.tsx @@ -49,12 +49,12 @@ const AccessConfigFormFieldsProvider1Panel = () => { diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderAPISIX.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderAPISIX.tsx index d6146cb66..328b40f0e 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderAPISIX.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderAPISIX.tsx @@ -39,12 +39,12 @@ const AccessConfigFormFieldsProviderAPISIX = () => { diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaWAF.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaWAF.tsx index 980534fdf..0f4e12bdf 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaWAF.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaWAF.tsx @@ -40,12 +40,12 @@ const AccessConfigFormFieldsProviderBaotaWAF = () => { diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderCdnfly.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderCdnfly.tsx index 7877b2ac6..85ae13e20 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderCdnfly.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderCdnfly.tsx @@ -49,12 +49,12 @@ const AccessConfigFormFieldsProviderCdnfly = () => { diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderFlexCDN.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderFlexCDN.tsx index 898f483e6..eb125ae01 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderFlexCDN.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderFlexCDN.tsx @@ -53,12 +53,12 @@ const AccessConfigFormFieldsProviderFlexCDN = () => { diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderGoEdge.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderGoEdge.tsx index 5f285f6a3..d71ca9a9a 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderGoEdge.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderGoEdge.tsx @@ -53,12 +53,12 @@ const AccessConfigFormFieldsProviderGoEdge = () => { diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderKong.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderKong.tsx index 1b84b385b..6949b1e41 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderKong.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderKong.tsx @@ -34,12 +34,12 @@ const AccessConfigFormFieldsProviderKong = () => { diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderLeCDN.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderLeCDN.tsx index e008b4a04..6b1558015 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderLeCDN.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderLeCDN.tsx @@ -45,12 +45,12 @@ const AccessConfigFormFieldsProviderLeCDN = () => { diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderProxmoxVE.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderProxmoxVE.tsx index 5767da947..a6658fed3 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderProxmoxVE.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderProxmoxVE.tsx @@ -49,12 +49,12 @@ const AccessConfigFormFieldsProviderProxmoxVE = () => { diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCDN.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCDN.tsx index d989a003d..543d28f86 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCDN.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCDN.tsx @@ -1,12 +1,17 @@ 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 MATCH_PATTERN_EXACT = "exact" as const; +const MATCH_PATTERN_WILDCARD = "wildcard" as const; +const MATCH_PATTERN_CERTSAN = "certsan" as const; + const BizDeployNodeConfigFieldsProviderTencentCloudCDN = () => { const { i18n, t } = useTranslation(); @@ -15,8 +20,11 @@ const BizDeployNodeConfigFieldsProviderTencentCloudCDN = () => { [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); + const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); + const fieldDomainPattern = Form.useWatch([parentNamePath, "matchPattern"], { form: formInst, preserve: true }); + return ( <> { } rules={[formRule]} - tooltip={} > - + ({ + key: s, + label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), + value: s, + }))} + /> + + + } + > + + + ); }; const getInitialValues = (): Nullish>> => { return { + matchPattern: MATCH_PATTERN_EXACT, domain: "", }; }; @@ -51,10 +78,33 @@ 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(), + matchPattern: z.enum( + [MATCH_PATTERN_EXACT, MATCH_PATTERN_WILDCARD, MATCH_PATTERN_CERTSAN], + t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder") + ), + domain: z.string().nullish(), + }) + .superRefine((values, ctx) => { + if (values.matchPattern) { + switch (values.matchPattern) { + case MATCH_PATTERN_EXACT: + case MATCH_PATTERN_WILDCARD: + { + if (!validDomainName(values.domain!, { allowWildcard: true })) { + ctx.addIssue({ + code: "custom", + message: t("common.errmsg.domain_invalid"), + path: ["domain"], + }); + } + } + break; + } + } + }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudCDN, { diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudECDN.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudECDN.tsx index 57cf9705c..a11672435 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudECDN.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudECDN.tsx @@ -1,12 +1,17 @@ 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 MATCH_PATTERN_EXACT = "exact" as const; +const MATCH_PATTERN_WILDCARD = "wildcard" as const; +const MATCH_PATTERN_CERTSAN = "certsan" as const; + const BizDeployNodeConfigFieldsProviderTencentCloudECDN = () => { const { i18n, t } = useTranslation(); @@ -15,8 +20,11 @@ const BizDeployNodeConfigFieldsProviderTencentCloudECDN = () => { [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); + const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); + const fieldDomainPattern = Form.useWatch([parentNamePath, "matchPattern"], { form: formInst, preserve: true }); + return ( <> { } rules={[formRule]} - tooltip={} > - + ({ + key: s, + label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), + value: s, + }))} + /> + + + } + > + + + ); }; const getInitialValues = (): Nullish>> => { return { + matchPattern: MATCH_PATTERN_EXACT, domain: "", }; }; @@ -51,10 +78,33 @@ 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(), + matchPattern: z.enum( + [MATCH_PATTERN_EXACT, MATCH_PATTERN_WILDCARD, MATCH_PATTERN_CERTSAN], + t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder") + ), + domain: z.string().nullish(), + }) + .superRefine((values, ctx) => { + if (values.matchPattern) { + switch (values.matchPattern) { + case MATCH_PATTERN_EXACT: + case MATCH_PATTERN_WILDCARD: + { + if (!validDomainName(values.domain!, { allowWildcard: true })) { + ctx.addIssue({ + code: "custom", + message: t("common.errmsg.domain_invalid"), + path: ["domain"], + }); + } + } + break; + } + } + }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudECDN, { diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineCDN.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineCDN.tsx index 423d201cc..910350400 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineCDN.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineCDN.tsx @@ -1,12 +1,17 @@ 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 MATCH_PATTERN_EXACT = "exact" as const; +const MATCH_PATTERN_WILDCARD = "wildcard" as const; +const MATCH_PATTERN_CERTSAN = "certsan" as const; + const BizDeployNodeConfigFieldsProviderVolcEngineCDN = () => { const { i18n, t } = useTranslation(); @@ -15,25 +20,47 @@ const BizDeployNodeConfigFieldsProviderVolcEngineCDN = () => { [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); + const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); + const fieldDomainPattern = Form.useWatch([parentNamePath, "matchPattern"], { form: formInst, preserve: true }); + return ( <> } rules={[formRule]} - tooltip={} > - + ({ + key: s, + label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), + value: s, + }))} + /> + + + } + > + + + ); }; const getInitialValues = (): Nullish>> => { return { + matchPattern: MATCH_PATTERN_EXACT, domain: "", }; }; @@ -41,9 +68,32 @@ const getInitialValues = (): Nullish>> => { const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; - return z.object({ - domain: z.string().refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); + return z + .object({ + matchPattern: z.enum( + [MATCH_PATTERN_EXACT, MATCH_PATTERN_WILDCARD, MATCH_PATTERN_CERTSAN], + t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder") + ), + domain: z.string().nullish(), + }) + .superRefine((values, ctx) => { + if (values.matchPattern) { + switch (values.matchPattern) { + case MATCH_PATTERN_EXACT: + case MATCH_PATTERN_WILDCARD: + { + if (!validDomainName(values.domain!, { allowWildcard: true })) { + ctx.addIssue({ + code: "custom", + message: t("common.errmsg.domain_invalid"), + path: ["domain"], + }); + } + } + break; + } + } + }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineCDN, { diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineLive.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineLive.tsx index d9030d5a8..3dbfbf790 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineLive.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineLive.tsx @@ -1,5 +1,5 @@ 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"; @@ -7,6 +7,9 @@ import { validDomainName } from "@/utils/validators"; import { useFormNestedFieldsContext } from "./_context"; +const MATCH_PATTERN_EXACT = "exact" as const; +const MATCH_PATTERN_WILDCARD = "wildcard" as const; + const BizDeployNodeConfigFieldsProviderVolcEngineLive = () => { const { i18n, t } = useTranslation(); @@ -19,6 +22,21 @@ const BizDeployNodeConfigFieldsProviderVolcEngineLive = () => { return ( <> + + ({ + key: s, + label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), + value: s, + }))} + /> + + { const getInitialValues = (): Nullish>> => { return { + matchPattern: MATCH_PATTERN_EXACT, domain: "", }; }; @@ -41,9 +60,29 @@ const getInitialValues = (): Nullish>> => { const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; - return z.object({ - domain: z.string().refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), - }); + return z + .object({ + matchPattern: z.enum([MATCH_PATTERN_EXACT, MATCH_PATTERN_WILDCARD], t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")), + domain: z.string().nullish(), + }) + .superRefine((values, ctx) => { + if (values.matchPattern) { + switch (values.matchPattern) { + case MATCH_PATTERN_EXACT: + case MATCH_PATTERN_WILDCARD: + { + if (!validDomainName(values.domain!, { allowWildcard: true })) { + ctx.addIssue({ + code: "custom", + message: t("common.errmsg.domain_invalid"), + path: ["domain"], + }); + } + } + break; + } + } + }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineLive, { diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index a37963a5f..8b285ccd2 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -42,6 +42,9 @@ "access.form.provider.placeholder": "Please select a provider", "access.form.provider.help": "DNS provider: The provider that hosts your domain names and manages your DNS records.
Hosting provider: The provider that hosts your servers or cloud services for deploying certificates.", "access.form.provider.search.placeholder": "Search provider ...", + "access.form.shared_allow_insecure_conns.label": "Insecure SSL/TLS connections", + "access.form.shared_allow_insecure_conns.switch.on": "Allow", + "access.form.shared_allow_insecure_conns.switch.off": "Disallow", "access.form.1panel_server_url.label": "1Panel server URL", "access.form.1panel_server_url.placeholder": "Please enter 1Panel server URL", "access.form.1panel_server_url.help": "Notes: DO NOT include the security entrance suffix.", @@ -50,9 +53,6 @@ "access.form.1panel_api_key.label": "1Panel API key", "access.form.1panel_api_key.placeholder": "Please enter 1Panel API key", "access.form.1panel_api_key.tooltip": "For more information, see https://docs.1panel.pro/dev_manual/api_manual/", - "access.form.common_allow_insecure_conns.label": "Insecure SSL/TLS connections", - "access.form.common_allow_insecure_conns.switch.on": "Allow", - "access.form.common_allow_insecure_conns.switch.off": "Disallow", "access.form.acmeca_endpoint.label": "Endpoint", "access.form.acmeca_endpoint.placeholder": "Please enter endpoint", "access.form.acmeca_endpoint.tooltip": "For more information, see https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 90b602fbd..0fbf3494f 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -166,6 +166,12 @@ "workflow_node.deploy.form.provider_access.label": "Hosting provider credential", "workflow_node.deploy.form.provider_access.placeholder": "Please select an credential of hosting provider", "workflow_node.deploy.form.provider_access.button": "Create", + "workflow_node.deploy.form.shared_domain_match_pattern.label": "Domain match pattern", + "workflow_node.deploy.form.shared_domain_match_pattern.placeholder": "Please select domain match pattern", + "workflow_node.deploy.form.shared_domain_match_pattern.option.exact.label": "Exact matches", + "workflow_node.deploy.form.shared_domain_match_pattern.option.wildcard.label": "Wildcard matches", + "workflow_node.deploy.form.shared_domain_match_pattern.option.certsan.label": "via Certificate", + "workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard": "Notes: For sites that support wildcards, exact matches will only apply to the site itself and will not include its subdomains.", "workflow_node.deploy.form.1panel_console_auto_restart.label": "Auto restart 1Panel after deployment", "workflow_node.deploy.form.1panel_site_node_name.label": "1Panel node name (Optional)", "workflow_node.deploy.form.1panel_site_node_name.placeholder": "Please enter 1Panel node name", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 16994e833..d48b50da9 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -41,9 +41,9 @@ "access.form.provider.placeholder": "请选择提供商", "access.form.provider.help": "提供商分为两种类型:
【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理域名解析记录。
【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。", "access.form.provider.search.placeholder": "搜索提供商……", - "access.form.common_allow_insecure_conns.label": "忽略 SSL/TLS 证书错误", - "access.form.common_allow_insecure_conns.switch.on": "允许", - "access.form.common_allow_insecure_conns.switch.off": "不允许", + "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.1panel_server_url.label": "1Panel 服务地址", "access.form.1panel_server_url.placeholder": "请输入 1Panel 服务地址", "access.form.1panel_server_url.help": "提示:请勿包含安全入口后缀。", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 98bddd2ec..3c921572f 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -165,6 +165,12 @@ "workflow_node.deploy.form.provider_access.label": "主机提供商授权", "workflow_node.deploy.form.provider_access.placeholder": "请选择主机提供商授权", "workflow_node.deploy.form.provider_access.button": "新建", + "workflow_node.deploy.form.shared_domain_match_pattern.label": "域名匹配模式", + "workflow_node.deploy.form.shared_domain_match_pattern.placeholder": "请选择域名匹配模式", + "workflow_node.deploy.form.shared_domain_match_pattern.option.exact.label": "精确匹配", + "workflow_node.deploy.form.shared_domain_match_pattern.option.wildcard.label": "通配符匹配(泛域名)", + "workflow_node.deploy.form.shared_domain_match_pattern.option.certsan.label": "通过证书自动匹配", + "workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard": "注意:对于支持泛解析的站点,精确匹配仅适用于该泛解析站点本身,而不包括符合条件的子域名。", "workflow_node.deploy.form.1panel_console_auto_restart.label": "部署后自动重启 1Panel 服务", "workflow_node.deploy.form.1panel_site_node_name.label": "1Panel 子节点名称(可选)", "workflow_node.deploy.form.1panel_site_node_name.placeholder": "请输入 1Panel 子节点名称",