feat: standardize domain match patterns on deployment

This commit is contained in:
Fu Diwei
2025-09-08 23:29:02 +08:00
parent 531840e59a
commit 5f27509b54
32 changed files with 796 additions and 184 deletions
@@ -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 {
@@ -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 {
@@ -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
@@ -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
@@ -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))
@@ -0,0 +1,10 @@
package tencentcloudcdn
const (
// 匹配模式:精确匹配。
MatchPatternExact = "exact"
// 匹配模式:通配符匹配。
MatchPatternWildcard = "wildcard"
// 匹配模式:证书 SAN 匹配。
MatchPatternCertSAN = "certsan"
)
@@ -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
}
@@ -0,0 +1,10 @@
package tencentcloudecdn
const (
// 匹配模式:精确匹配。
MatchPatternExact = "exact"
// 匹配模式:通配符匹配。
MatchPatternWildcard = "wildcard"
// 匹配模式:证书 SAN 匹配。
MatchPatternCertSAN = "certsan"
)
@@ -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()
@@ -0,0 +1,10 @@
package volcenginecdn
const (
// 匹配模式:精确匹配。
MatchPatternExact = "exact"
// 匹配模式:通配符匹配。
MatchPatternWildcard = "wildcard"
// 匹配模式:证书 SAN 匹配。
MatchPatternCertSAN = "certsan"
)
@@ -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
}
@@ -0,0 +1,8 @@
package volcenginelive
const (
// 匹配模式:精确匹配。
MatchPatternExact = "exact"
// 匹配模式:通配符匹配。
MatchPatternWildcard = "wildcard"
)
@@ -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
}
+33
View File
@@ -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
}
+50
View File
@@ -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)
}
})
}
@@ -49,12 +49,12 @@ const AccessConfigFormFieldsProvider1Panel = () => {
<Form.Item
name={[parentNamePath, "allowInsecureConnections"]}
initialValue={initialValues.allowInsecureConnections}
label={t("access.form.common_allow_insecure_conns.label")}
label={t("access.form.shared_allow_insecure_conns.label")}
rules={[formRule]}
>
<Switch
checkedChildren={t("access.form.common_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.common_allow_insecure_conns.switch.off")}
checkedChildren={t("access.form.shared_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.shared_allow_insecure_conns.switch.off")}
/>
</Form.Item>
</>
@@ -39,12 +39,12 @@ const AccessConfigFormFieldsProviderAPISIX = () => {
<Form.Item
name={[parentNamePath, "allowInsecureConnections"]}
initialValue={initialValues.allowInsecureConnections}
label={t("access.form.common_allow_insecure_conns.label")}
label={t("access.form.shared_allow_insecure_conns.label")}
rules={[formRule]}
>
<Switch
checkedChildren={t("access.form.common_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.common_allow_insecure_conns.switch.off")}
checkedChildren={t("access.form.shared_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.shared_allow_insecure_conns.switch.off")}
/>
</Form.Item>
</>
@@ -40,12 +40,12 @@ const AccessConfigFormFieldsProviderBaotaWAF = () => {
<Form.Item
name={[parentNamePath, "allowInsecureConnections"]}
initialValue={initialValues.allowInsecureConnections}
label={t("access.form.common_allow_insecure_conns.label")}
label={t("access.form.shared_allow_insecure_conns.label")}
rules={[formRule]}
>
<Switch
checkedChildren={t("access.form.common_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.common_allow_insecure_conns.switch.off")}
checkedChildren={t("access.form.shared_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.shared_allow_insecure_conns.switch.off")}
/>
</Form.Item>
</>
@@ -49,12 +49,12 @@ const AccessConfigFormFieldsProviderCdnfly = () => {
<Form.Item
name={[parentNamePath, "allowInsecureConnections"]}
initialValue={initialValues.allowInsecureConnections}
label={t("access.form.common_allow_insecure_conns.label")}
label={t("access.form.shared_allow_insecure_conns.label")}
rules={[formRule]}
>
<Switch
checkedChildren={t("access.form.common_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.common_allow_insecure_conns.switch.off")}
checkedChildren={t("access.form.shared_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.shared_allow_insecure_conns.switch.off")}
/>
</Form.Item>
</>
@@ -53,12 +53,12 @@ const AccessConfigFormFieldsProviderFlexCDN = () => {
<Form.Item
name={[parentNamePath, "allowInsecureConnections"]}
initialValue={initialValues.allowInsecureConnections}
label={t("access.form.common_allow_insecure_conns.label")}
label={t("access.form.shared_allow_insecure_conns.label")}
rules={[formRule]}
>
<Switch
checkedChildren={t("access.form.common_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.common_allow_insecure_conns.switch.off")}
checkedChildren={t("access.form.shared_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.shared_allow_insecure_conns.switch.off")}
/>
</Form.Item>
</>
@@ -53,12 +53,12 @@ const AccessConfigFormFieldsProviderGoEdge = () => {
<Form.Item
name={[parentNamePath, "allowInsecureConnections"]}
initialValue={initialValues.allowInsecureConnections}
label={t("access.form.common_allow_insecure_conns.label")}
label={t("access.form.shared_allow_insecure_conns.label")}
rules={[formRule]}
>
<Switch
checkedChildren={t("access.form.common_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.common_allow_insecure_conns.switch.off")}
checkedChildren={t("access.form.shared_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.shared_allow_insecure_conns.switch.off")}
/>
</Form.Item>
</>
@@ -34,12 +34,12 @@ const AccessConfigFormFieldsProviderKong = () => {
<Form.Item
name={[parentNamePath, "allowInsecureConnections"]}
initialValue={initialValues.allowInsecureConnections}
label={t("access.form.common_allow_insecure_conns.label")}
label={t("access.form.shared_allow_insecure_conns.label")}
rules={[formRule]}
>
<Switch
checkedChildren={t("access.form.common_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.common_allow_insecure_conns.switch.off")}
checkedChildren={t("access.form.shared_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.shared_allow_insecure_conns.switch.off")}
/>
</Form.Item>
</>
@@ -45,12 +45,12 @@ const AccessConfigFormFieldsProviderLeCDN = () => {
<Form.Item
name={[parentNamePath, "allowInsecureConnections"]}
initialValue={initialValues.allowInsecureConnections}
label={t("access.form.common_allow_insecure_conns.label")}
label={t("access.form.shared_allow_insecure_conns.label")}
rules={[formRule]}
>
<Switch
checkedChildren={t("access.form.common_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.common_allow_insecure_conns.switch.off")}
checkedChildren={t("access.form.shared_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.shared_allow_insecure_conns.switch.off")}
/>
</Form.Item>
</>
@@ -49,12 +49,12 @@ const AccessConfigFormFieldsProviderProxmoxVE = () => {
<Form.Item
name={[parentNamePath, "allowInsecureConnections"]}
initialValue={initialValues.allowInsecureConnections}
label={t("access.form.common_allow_insecure_conns.label")}
label={t("access.form.shared_allow_insecure_conns.label")}
rules={[formRule]}
>
<Switch
checkedChildren={t("access.form.common_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.common_allow_insecure_conns.switch.off")}
checkedChildren={t("access.form.shared_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.shared_allow_insecure_conns.switch.off")}
/>
</Form.Item>
</>
@@ -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 (
<>
<Form.Item
@@ -30,20 +38,39 @@ const BizDeployNodeConfigFieldsProviderTencentCloudCDN = () => {
</Form.Item>
<Form.Item
name={[parentNamePath, "domain"]}
initialValue={initialValues.domain}
label={t("workflow_node.deploy.form.tencentcloud_cdn_domain.label")}
name={[parentNamePath, "matchPattern"]}
initialValue={initialValues.matchPattern}
label={t("workflow_node.deploy.form.shared_domain_match_pattern.label")}
extra={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard") }}></span>}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_cdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_cdn_domain.placeholder")} />
<Radio.Group
options={[MATCH_PATTERN_EXACT, MATCH_PATTERN_WILDCARD, MATCH_PATTERN_CERTSAN].map((s) => ({
key: s,
label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),
value: s,
}))}
/>
</Form.Item>
<Show when={fieldDomainPattern !== MATCH_PATTERN_CERTSAN}>
<Form.Item
name={[parentNamePath, "domain"]}
initialValue={initialValues.domain}
label={t("workflow_node.deploy.form.tencentcloud_cdn_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_cdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_cdn_domain.placeholder")} />
</Form.Item>
</Show>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
matchPattern: MATCH_PATTERN_EXACT,
domain: "",
};
};
@@ -51,10 +78,33 @@ const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
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, {
@@ -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 (
<>
<Form.Item
@@ -30,20 +38,39 @@ const BizDeployNodeConfigFieldsProviderTencentCloudECDN = () => {
</Form.Item>
<Form.Item
name={[parentNamePath, "domain"]}
initialValue={initialValues.domain}
label={t("workflow_node.deploy.form.tencentcloud_ecdn_domain.label")}
name={[parentNamePath, "matchPattern"]}
initialValue={initialValues.matchPattern}
label={t("workflow_node.deploy.form.shared_domain_match_pattern.label")}
extra={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard") }}></span>}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_ecdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_ecdn_domain.placeholder")} />
<Radio.Group
options={[MATCH_PATTERN_EXACT, MATCH_PATTERN_WILDCARD, MATCH_PATTERN_CERTSAN].map((s) => ({
key: s,
label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),
value: s,
}))}
/>
</Form.Item>
<Show when={fieldDomainPattern !== MATCH_PATTERN_CERTSAN}>
<Form.Item
name={[parentNamePath, "domain"]}
initialValue={initialValues.domain}
label={t("workflow_node.deploy.form.tencentcloud_ecdn_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_ecdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_ecdn_domain.placeholder")} />
</Form.Item>
</Show>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
matchPattern: MATCH_PATTERN_EXACT,
domain: "",
};
};
@@ -51,10 +78,33 @@ const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
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, {
@@ -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 (
<>
<Form.Item
name={[parentNamePath, "domain"]}
initialValue={initialValues.domain}
label={t("workflow_node.deploy.form.volcengine_cdn_domain.label")}
name={[parentNamePath, "matchPattern"]}
initialValue={initialValues.matchPattern}
label={t("workflow_node.deploy.form.shared_domain_match_pattern.label")}
extra={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.shared_domain_match_pattern.help_wildcard") }}></span>}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.volcengine_cdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.volcengine_cdn_domain.placeholder")} />
<Radio.Group
options={[MATCH_PATTERN_EXACT, MATCH_PATTERN_WILDCARD, MATCH_PATTERN_CERTSAN].map((s) => ({
key: s,
label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),
value: s,
}))}
/>
</Form.Item>
<Show when={fieldDomainPattern !== MATCH_PATTERN_CERTSAN}>
<Form.Item
name={[parentNamePath, "domain"]}
initialValue={initialValues.domain}
label={t("workflow_node.deploy.form.volcengine_cdn_domain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.volcengine_cdn_domain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.volcengine_cdn_domain.placeholder")} />
</Form.Item>
</Show>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
matchPattern: MATCH_PATTERN_EXACT,
domain: "",
};
};
@@ -41,9 +68,32 @@ const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
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, {
@@ -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 (
<>
<Form.Item
name={[parentNamePath, "matchPattern"]}
initialValue={initialValues.matchPattern}
label={t("workflow_node.deploy.form.shared_domain_match_pattern.label")}
rules={[formRule]}
>
<Radio.Group
options={[MATCH_PATTERN_EXACT, MATCH_PATTERN_WILDCARD].map((s) => ({
key: s,
label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),
value: s,
}))}
/>
</Form.Item>
<Form.Item
name={[parentNamePath, "domain"]}
initialValue={initialValues.domain}
@@ -34,6 +52,7 @@ const BizDeployNodeConfigFieldsProviderVolcEngineLive = () => {
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
matchPattern: MATCH_PATTERN_EXACT,
domain: "",
};
};
@@ -41,9 +60,29 @@ const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
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, {
+3 -3
View File
@@ -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. <br>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 <a href=\"https://docs.1panel.pro/dev_manual/api_manual/\" target=\"_blank\">https://docs.1panel.pro/dev_manual/api_manual/</a>",
"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 <a href=\"https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1\" target=\"_blank\">https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1</a>",
@@ -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, <strong>exact matches</strong> 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",
+3 -3
View File
@@ -41,9 +41,9 @@
"access.form.provider.placeholder": "请选择提供商",
"access.form.provider.help": "提供商分为两种类型:<br>【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理域名解析记录。<br>【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。",
"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": "提示:请勿包含安全入口后缀。",
@@ -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": "注意:对于支持泛解析的站点,<strong>精确匹配</strong>仅适用于该泛解析站点本身,而不包括符合条件的子域名。",
"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 子节点名称",