diff --git a/go.mod b/go.mod index 1b2b2ef68..9e86801fe 100644 --- a/go.mod +++ b/go.mod @@ -27,22 +27,22 @@ require ( github.com/alibabacloud-go/tea v1.5.2 github.com/alibabacloud-go/tea-utils/v2 v2.0.9 github.com/alibabacloud-go/vod-20170321/v4 v4.11.3 - github.com/alibabacloud-go/waf-openapi-20211001/v7 v7.8.2 - github.com/aws/aws-sdk-go-v2 v1.42.1 - github.com/aws/aws-sdk-go-v2/config v1.32.29 - github.com/aws/aws-sdk-go-v2/credentials v1.19.28 - github.com/aws/aws-sdk-go-v2/service/acm v1.42.0 - github.com/aws/aws-sdk-go-v2/service/amplify v1.40.0 - github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.36.0 - github.com/aws/aws-sdk-go-v2/service/cloudfront v1.66.0 - github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.35.0 - github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.56.0 - github.com/aws/aws-sdk-go-v2/service/iam v1.55.0 - github.com/aws/aws-sdk-go-v2/service/lightsail v1.57.0 - github.com/aws/smithy-go v1.27.3 - github.com/baidubce/bce-sdk-go v0.9.270 - github.com/byteplus-sdk/byteplus-go-sdk-v2 v1.0.71 - github.com/byteplus-sdk/byteplus-sdk-golang v1.0.71 + github.com/alibabacloud-go/waf-openapi-20211001/v7 v7.8.1 + github.com/aws/aws-sdk-go-v2 v1.41.12 + github.com/aws/aws-sdk-go-v2/config v1.32.23 + github.com/aws/aws-sdk-go-v2/credentials v1.19.22 + github.com/aws/aws-sdk-go-v2/service/acm v1.39.5 + github.com/aws/aws-sdk-go-v2/service/amplify v1.39.3 + github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.35.5 + github.com/aws/aws-sdk-go-v2/service/cloudfront v1.65.1 + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.34.5 + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.55.3 + github.com/aws/aws-sdk-go-v2/service/iam v1.54.3 + github.com/aws/aws-sdk-go-v2/service/lightsail v1.55.3 + github.com/aws/smithy-go v1.27.1 + github.com/baidubce/bce-sdk-go v0.9.268 + github.com/byteplus-sdk/byteplus-go-sdk-v2 v1.0.66 + github.com/byteplus-sdk/byteplus-sdk-golang v1.0.68 github.com/go-acme/lego/v5 v5.2.2 github.com/go-cmd/cmd v1.4.3 github.com/go-resty/resty/v2 v2.17.2 @@ -177,16 +177,16 @@ require ( github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect github.com/aliyun/credentials-go v1.4.12 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.4.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.32.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.44.0 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.28 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.28 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.28 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.28 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.1.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.2 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bodgit/gssapi v0.0.3 // indirect github.com/bodgit/tsig v1.3.1 // indirect diff --git a/internal/certmgmt/deployers/sp_tencentcloud_eomakers.go b/internal/certmgmt/deployers/sp_tencentcloud_eomakers.go new file mode 100644 index 000000000..c3648a9cb --- /dev/null +++ b/internal/certmgmt/deployers/sp_tencentcloud_eomakers.go @@ -0,0 +1,37 @@ +package deployers + +import ( + "fmt" + "strings" + + "github.com/samber/lo" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/pkg/core" + dplyimpl "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-eomakers" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + Registries.MustRegister( + domain.DeploymentProviderTypeTencentCloudEOMakers, + func(options *ProviderFactoryOptions) (core.Deployer, error) { + credentials := domain.AccessConfigForTencentCloud{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := dplyimpl.NewDeployer(&dplyimpl.DeployerConfig{ + SecretId: credentials.SecretId, + SecretKey: credentials.SecretKey, + ProjectId: credentials.ProjectId, + Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), + MakersApiToken: xmaps.GetString(options.ProviderExtendedConfig, "apiToken"), + MakersProjectId: xmaps.GetString(options.ProviderExtendedConfig, "projectId"), + DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), + Domains: lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, "domains"), ";"), func(s string, _ int) bool { return s != "" }), + EnableMultipleSSL: xmaps.GetBool(options.ProviderExtendedConfig, "enableMultipleSSL"), + }) + return provider, err + }) +} diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 226bc6907..5aa121fba 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -429,6 +429,7 @@ const ( DeploymentProviderTypeTencentCloudCSS = DeploymentProviderType(AccessProviderTypeTencentCloud + "-css") DeploymentProviderTypeTencentCloudECDN = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ecdn") DeploymentProviderTypeTencentCloudEO = DeploymentProviderType(AccessProviderTypeTencentCloud + "-eo") + DeploymentProviderTypeTencentCloudEOMakers = DeploymentProviderType(AccessProviderTypeTencentCloud + "-eomakers") DeploymentProviderTypeTencentCloudGA2 = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ga2") DeploymentProviderTypeTencentCloudGAAP = DeploymentProviderType(AccessProviderTypeTencentCloud + "-gaap") DeploymentProviderTypeTencentCloudSCF = DeploymentProviderType(AccessProviderTypeTencentCloud + "-scf") diff --git a/pkg/core/deployer/providers/tencentcloud-eomakers/consts.go b/pkg/core/deployer/providers/tencentcloud-eomakers/consts.go new file mode 100644 index 000000000..7dbcad4ac --- /dev/null +++ b/pkg/core/deployer/providers/tencentcloud-eomakers/consts.go @@ -0,0 +1,7 @@ +package tencentcloudeomakers + +const ( + DomainMatchPatternExact = "exact" + DomainMatchPatternWildcard = "wildcard" + DomainMatchPatternCertSan = "certsan" +) diff --git a/pkg/core/deployer/providers/tencentcloud-eomakers/tencentcloud_eomakers.go b/pkg/core/deployer/providers/tencentcloud-eomakers/tencentcloud_eomakers.go new file mode 100644 index 000000000..d3fbe66bc --- /dev/null +++ b/pkg/core/deployer/providers/tencentcloud-eomakers/tencentcloud_eomakers.go @@ -0,0 +1,357 @@ +package tencentcloudeomakers + +import ( + "context" + "crypto/x509" + "errors" + "fmt" + "log/slog" + "strings" + "time" + + "github.com/samber/lo" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + + tceo "github.com/certimate-go/certimate/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901" + + "github.com/certimate-go/certimate/pkg/core" + cmgrimpl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl" + tceomakersssdk "github.com/certimate-go/certimate/pkg/sdk3rd/tencentcloud/teomakers" + xcert "github.com/certimate-go/certimate/pkg/utils/cert" + xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" + xcertkey "github.com/certimate-go/certimate/pkg/utils/cert/key" + xtencentcloud "github.com/certimate-go/certimate/pkg/utils/third-party/tencentcloud" +) + +type ( + Provider = core.Deployer + DeployResult = core.DeployerDeployResult +) + +type DeployerConfig struct { + // 腾讯云 SecretId。 + SecretId string `json:"secretId"` + // 腾讯云 SecretKey。 + SecretKey string `json:"secretKey"` + // 腾讯云项目 ID。 + ProjectId int64 `json:"projectId,omitempty"` + // 腾讯云接口端点。 + Endpoint string `json:"endpoint,omitempty"` + // EdgeOne Makers API Token。 + MakersApiToken string `json:"makersApiToken"` + // EdgeOne Makers 项目 ID。 + MakersProjectId string `json:"makersProjectId"` + // 域名匹配模式。 + // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 + DomainMatchPattern string `json:"domainMatchPattern,omitempty"` + // 加速域名列表(支持泛域名)。 + Domains []string `json:"domains"` + // 是否启用多证书模式。 + EnableMultipleSSL bool `json:"enableMultipleSSL,omitempty"` +} + +type Deployer struct { + config *DeployerConfig + logger *slog.Logger + sdkClients *wSDKClients + sdkCertmgr core.Certmgr +} + +var _ Provider = (*Deployer)(nil) + +type wSDKClients struct { + TEO *tceo.Client + TEOMakers *tceomakersssdk.Client +} + +func NewDeployer(config *DeployerConfig) (*Deployer, error) { + if config == nil { + return nil, fmt.Errorf("the configuration of the deployer provider is nil") + } + + clients, err := createSDKClients(config.SecretId, config.SecretKey, config.Endpoint, config.MakersApiToken) + if err != nil { + return nil, fmt.Errorf("could not create client: %w", err) + } + + pcertmgr, err := cmgrimpl.NewCertmgr(&cmgrimpl.CertmgrConfig{ + SecretId: config.SecretId, + SecretKey: config.SecretKey, + ProjectId: config.ProjectId, + Endpoint: lo.Ternary(xtencentcloud.IsIntlAPIEndpoint(config.Endpoint), "ssl.intl.tencentcloudapi.com", ""), + }) + if err != nil { + return nil, fmt.Errorf("could not create certmgr: %w", err) + } + + return &Deployer{ + config: config, + logger: slog.Default(), + sdkClients: clients, + sdkCertmgr: pcertmgr, + }, nil +} + +func (d *Deployer) SetLogger(logger *slog.Logger) { + if logger == nil { + d.logger = slog.New(slog.DiscardHandler) + } else { + d.logger = logger + } + + d.sdkCertmgr.SetLogger(logger) +} + +func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) { + if d.config.MakersProjectId == "" { + return nil, fmt.Errorf("config `makersProjectId` is required") + } + + // 上传证书 + upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) + if err != nil { + return nil, fmt.Errorf("failed to upload certificate file: %w", err) + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + + // 获取全部可部署的域名列表 + domainsInMakers, err := d.getAllDomainsInProject(ctx, d.config.MakersProjectId) + if err != nil { + return nil, err + } + + // 获取待部署的域名列表 + var domains []string + switch d.config.DomainMatchPattern { + case "", DomainMatchPatternExact: + { + if len(d.config.Domains) == 0 { + return nil, fmt.Errorf("config `domains` is required") + } + + domains = d.config.Domains + } + + case DomainMatchPatternWildcard: + { + if len(d.config.Domains) == 0 { + return nil, fmt.Errorf("config `domains` is required") + } + + domainCandidates := lo.Map(domainsInMakers, func(domainInfo *tceomakersssdk.PagesZoneCustomDomain, _ int) string { + return lo.FromPtr(domainInfo.Domain) + }) + domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { + for _, configDomain := range d.config.Domains { + if xcerthostname.IsMatch(configDomain, domain) { + return true + } + } + return false + }) + if len(domains) == 0 { + return nil, fmt.Errorf("could not find any domains matched by wildcard") + } + } + + case DomainMatchPatternCertSan: + { + domainCandidates := lo.Map(domainsInMakers, func(domainInfo *tceomakersssdk.PagesZoneCustomDomain, _ int) string { + return lo.FromPtr(domainInfo.Domain) + }) + domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { + return xcerthostname.IsMatchByCertificatePEM(certPEM, domain) + }) + if len(domains) == 0 { + return nil, fmt.Errorf("could not find any domains matched by certificate") + } + } + + default: + return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) + } + + // 批量更新域名证书 + if len(domains) == 0 { + d.logger.Info("no edgeone makers domains to deploy") + } else { + d.logger.Info("found edgeone makers domains to deploy", slog.Any("domains", domains)) + + // 获取站点 ID + zoneId := domainsInMakers[0].ZoneId + + // 获取证书列表 + describeHostCertificatesReq := tceo.NewDescribeHostCertificatesRequest() + describeHostCertificatesReq.ZoneId = zoneId + describeHostCertificatesResp, err := d.sdkClients.TEO.DescribeHostCertificatesWithContext(ctx, describeHostCertificatesReq) + d.logger.Debug("sdk request 'teo.DescribeHostCertificates'", slog.Any("request", describeHostCertificatesReq), slog.Any("response", describeHostCertificatesResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'teo.DescribeHostCertificates': %w", err) + } + + // 跳过已部署过的域名 + domains = lo.Filter(domains, func(domain string, _ int) bool { + var deployed bool + + domainInfo, _ := lo.Find(domainsInMakers, func(domainInfo *tceomakersssdk.PagesZoneCustomDomain) bool { + return domain == lo.FromPtr(domainInfo.Domain) + }) + if domainInfo != nil && describeHostCertificatesResp.Response != nil { + deployed = lo.SomeBy(describeHostCertificatesResp.Response.HostCertificates, func(hostInfo *tceo.HostCertificate) bool { + return domain == lo.FromPtr(hostInfo.Host) && + lo.SomeBy(hostInfo.HostCertInfo, func(certInfo *tceo.CertificateInfo) bool { + return upres.CertId == lo.FromPtr(certInfo.CertId) + }) + }) + } + + return !deployed + }) + + // 配置域名证书 + // REF: https://cloud.tencent.com/document/api/1552/80764 + modifyHostsCertificateReqs := make([]*tceo.ModifyHostsCertificateRequest, 0) + + if d.config.EnableMultipleSSL { + const algRSA = "RSA" + const algECC = "ECC" + + privkey, err := xcert.ParsePrivateKeyFromPEM(privkeyPEM) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + + privkeyAlg, _, _ := xcertkey.GetPrivateKeyAlgorithm(privkey) + privkeyAlgStr := "" + switch privkeyAlg { + case x509.RSA: + privkeyAlgStr = algRSA + case x509.ECDSA: + privkeyAlgStr = algECC + } + + for _, domain := range domains { + modifyHostsCertificateReq := tceo.NewModifyHostsCertificateRequest() + modifyHostsCertificateReq.ZoneId = zoneId + modifyHostsCertificateReq.Mode = common.StringPtr("sslcert") + modifyHostsCertificateReq.Hosts = common.StringPtrs([]string{domain}) + modifyHostsCertificateReq.ServerCertInfo = []*tceo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}} + + domainInfo, _ := lo.Find(domainsInMakers, func(domainInfo *tceomakersssdk.PagesZoneCustomDomain) bool { + return domain == lo.FromPtr(domainInfo.Domain) + }) + if domainInfo != nil && describeHostCertificatesResp.Response != nil { + for _, hostInfo := range describeHostCertificatesResp.Response.HostCertificates { + if lo.FromPtr(hostInfo.Host) != domain { + continue + } + + for _, certInfo := range hostInfo.HostCertInfo { + if lo.FromPtr(certInfo.CertId) == upres.CertId { + continue + } + + if strings.Split(lo.FromPtr(certInfo.SignAlgo), " ")[0] == privkeyAlgStr { + continue + } + + certExpireTime, _ := time.Parse("2006-01-02T15:04:05Z", lo.FromPtr(certInfo.ExpireTime)) + if certExpireTime.Before(time.Now()) { + continue + } + + modifyHostsCertificateReq.ServerCertInfo = append(modifyHostsCertificateReq.ServerCertInfo, &tceo.ServerCertInfo{CertId: certInfo.CertId}) + } + } + } + + modifyHostsCertificateReqs = append(modifyHostsCertificateReqs, modifyHostsCertificateReq) + } + } else { + modifyHostsCertificateReq := tceo.NewModifyHostsCertificateRequest() + modifyHostsCertificateReq.ZoneId = zoneId + modifyHostsCertificateReq.Mode = common.StringPtr("sslcert") + modifyHostsCertificateReq.Hosts = common.StringPtrs(domains) + modifyHostsCertificateReq.ServerCertInfo = []*tceo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}} + + modifyHostsCertificateReqs = append(modifyHostsCertificateReqs, modifyHostsCertificateReq) + } + + var errs []error + for _, modifyHostsCertificateReq := range modifyHostsCertificateReqs { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + modifyHostsCertificateResp, err := d.sdkClients.TEO.ModifyHostsCertificateWithContext(ctx, modifyHostsCertificateReq) + d.logger.Debug("sdk request 'teo.ModifyHostsCertificate'", slog.Any("request", modifyHostsCertificateReq), slog.Any("response", modifyHostsCertificateResp)) + if err != nil { + err = fmt.Errorf("failed to execute sdk request 'teo.ModifyHostsCertificate': %w", err) + errs = append(errs, err) + } + } + } + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + } + + return &DeployResult{}, nil +} + +func (d *Deployer) getAllDomainsInProject(ctx context.Context, makersProjectId string) ([]*tceomakersssdk.PagesZoneCustomDomain, error) { + // 查询创建过的 Pages 自定义域名列表 + // REF: https://docs.edgeone.site/#/?id=describepageszonecustomdomains + describeMakersZoneCustomDomainsReq := &tceomakersssdk.DescribePagesZoneCustomDomainsRequest{ + ProjectId: common.StringPtr(makersProjectId), + } + describeMakersZoneCustomDomainsResp, err := d.sdkClients.TEOMakers.DescribePagesZoneCustomDomainsWithContext(ctx, describeMakersZoneCustomDomainsReq) + d.logger.Debug("api request 'teo.makers.DescribePagesZoneCustomDomains'", slog.Any("request", describeMakersZoneCustomDomainsReq), slog.Any("response", describeMakersZoneCustomDomainsResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'teo.makers.DescribePagesZoneCustomDomains': %w", err) + } + + domains := make([]*tceomakersssdk.PagesZoneCustomDomain, 0) + for _, domainItem := range describeMakersZoneCustomDomainsResp.Data.Response.PagesDomains { + if lo.FromPtr(domainItem.Type) == "Custom" && lo.FromPtr(domainItem.ZoneId) != "" { + domains = append(domains, domainItem) + } + } + + return domains, nil +} + +func createSDKClients(secretId, secretKey, endpoint, makersApiToken string) (*wSDKClients, error) { + wsdk := &wSDKClients{} + + { + credential := common.NewCredential(secretId, secretKey) + + cpf := profile.NewClientProfile() + if endpoint != "" { + cpf.HttpProfile.Endpoint = endpoint + } + + client, err := tceo.NewClient(credential, "", cpf) + if err != nil { + return nil, err + } + + wsdk.TEO = client + } + + { + client, err := tceomakersssdk.NewClient( + tceomakersssdk.WithApiToken(makersApiToken), + ) + if err != nil { + return nil, err + } + + wsdk.TEOMakers = client + } + + return wsdk, nil +} diff --git a/pkg/core/deployer/providers/tencentcloud-eomakers/tencentcloud_eomakers_test.go b/pkg/core/deployer/providers/tencentcloud-eomakers/tencentcloud_eomakers_test.go new file mode 100644 index 000000000..f95b01aa3 --- /dev/null +++ b/pkg/core/deployer/providers/tencentcloud-eomakers/tencentcloud_eomakers_test.go @@ -0,0 +1,64 @@ +package tencentcloudeomakers_test + +import ( + "strings" + "testing" + + "github.com/certimate-go/certimate/pkg/core/deployer/internal/tester" + impl "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-eomakers" +) + +var ( + fp = tester.Args("TENCENTCLOUDEOMAKERS_") + fTestCertPath string + fTestKeyPath string + fSecretId string + fSecretKey string + fMakersApiToken string + fMakersProjectId string + fDomains string +) + +func init() { + fp.DefineString(&fTestCertPath, "TESTCERTPATH") + fp.DefineString(&fTestKeyPath, "TESTKEYPATH") + fp.DefineString(&fSecretId, "SECRETID") + fp.DefineString(&fSecretKey, "SECRETKEY") + fp.DefineString(&fMakersApiToken, "MAKERSAPITOKEN") + fp.DefineString(&fMakersProjectId, "MAKERSPROJECTID") + fp.DefineString(&fDomains, "DOMAINS") +} + +/* +Shell command to run this test: + + go test -v ./tencentcloud_eomakers_test.go -args \ + --TENCENTCLOUDEOMAKERS_TESTCERTPATH="/path/to/your-test-cert.pem" \ + --TENCENTCLOUDEOMAKERS_TESTKEYPATH="/path/to/your-test-key.pem" \ + --TENCENTCLOUDEOMAKERS_SECRETID="your-secret-id" \ + --TENCENTCLOUDEOMAKERS_SECRETKEY="your-secret-key" \ + --TENCENTCLOUDEOMAKERS_MAKERSAPITOKEN="your-makers-api-token" \ + --TENCENTCLOUDEOMAKERS_MAKERSPROJECTID="your-makers-project-id" \ + --TENCENTCLOUDEOMAKERS_DOMAINS="example.com" +*/ +func TestProvider(t *testing.T) { + fp.Parse() + + t.Run("Deploy", func(t *testing.T) { + provider, err := impl.NewDeployer(&impl.DeployerConfig{ + SecretId: fSecretId, + SecretKey: fSecretKey, + MakersApiToken: fMakersApiToken, + MakersProjectId: fMakersProjectId, + DomainMatchPattern: impl.DomainMatchPatternExact, + Domains: strings.Split(fDomains, ";"), + EnableMultipleSSL: true, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + tester.TestDeploy(t, provider, tester.TestDeployArgs{CertPath: fTestCertPath, KeyPath: fTestKeyPath}) + }) +} diff --git a/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901/client.go b/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901/client.go index 260dade60..59e687c15 100644 --- a/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901/client.go +++ b/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901/client.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" teo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901" ) @@ -48,6 +49,41 @@ func (c *Client) DescribeAccelerationDomainsWithContext(ctx context.Context, req return } +func NewDescribeHostCertificatesRequest() (request *DescribeHostCertificatesRequest) { + request = &DescribeHostCertificatesRequest{ + BaseRequest: &tchttp.BaseRequest{}, + } + + request.Init().WithApiInfo("teo", APIVersion, "DescribeHostCertificates") + + return +} + +func NewDescribeHostCertificatesResponse() (response *DescribeHostCertificatesResponse) { + response = &DescribeHostCertificatesResponse{ + BaseResponse: &tchttp.BaseResponse{}, + } + + return +} + +func (c *Client) DescribeHostCertificatesWithContext(ctx context.Context, request *DescribeHostCertificatesRequest) (response *DescribeHostCertificatesResponse, err error) { + if request == nil { + request = NewDescribeHostCertificatesRequest() + } + c.InitBaseRequest(&request.BaseRequest, "teo", APIVersion, "DescribeHostCertificates") + + if c.GetCredential() == nil { + return nil, errors.New("DescribeHostCertificates require credential") + } + + request.SetContext(ctx) + + response = NewDescribeHostCertificatesResponse() + err = c.Send(request, response) + return +} + func NewModifyHostsCertificateRequest() (request *ModifyHostsCertificateRequest) { return teo.NewModifyHostsCertificateRequest() } diff --git a/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901/models.go b/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901/models.go index af53a87c3..fade70390 100644 --- a/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901/models.go +++ b/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901/models.go @@ -1,19 +1,50 @@ package v20220901 import ( + tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http" teo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901" ) type ( AccelerationDomain = teo.AccelerationDomain + AdvancedFilter = teo.AdvancedFilter CertificateInfo = teo.CertificateInfo + MutualTLS = teo.MutualTLS ServerCertInfo = teo.ServerCertInfo + UpstreamCertInfo = teo.UpstreamCertInfo ) type DescribeAccelerationDomainsRequest = teo.DescribeAccelerationDomainsRequest type DescribeAccelerationDomainsResponse = teo.DescribeAccelerationDomainsResponse +type DescribeHostCertificatesRequest struct { + *tchttp.BaseRequest + ZoneId *string `json:"ZoneId,omitnil,omitempty" name:"ZoneId"` + Filters []*AdvancedFilter `json:"Filters,omitnil,omitempty" name:"Filters"` +} + +type DescribeHostCertificatesResponse struct { + *tchttp.BaseResponse + Response *DescribeHostCertificatesResponseParams `json:"Response" name:"Response"` +} + +type DescribeHostCertificatesResponseParams struct { + RequestId *string `json:"RequestId,omitnil,omitempty" name:"RequestId"` + TotalCount *int64 `json:"TotalCount,omitnil,omitempty" name:"TotalCount"` + HostCertificates []*HostCertificate `json:"HostCertificates,omitnil,omitempty" name:"HostCertificates"` +} + type ModifyHostsCertificateRequest = teo.ModifyHostsCertificateRequest type ModifyHostsCertificateResponse = teo.ModifyHostsCertificateResponse + +type HostCertificate struct { + ApplyType *string `json:"ApplyType,omitnil,omitempty" name:"ApplyType"` + ClientCertInfo *MutualTLS `json:"ClientCertInfo,omitnil,omitempty" name:"ClientCertInfo"` + Host *string `json:"Host,omitnil,omitempty" name:"Host"` + HostCertInfo []*CertificateInfo `json:"HostCertInfo,omitnil,omitempty" name:"HostCertInfo"` + Mode *string `json:"Mode,omitnil,omitempty" name:"Mode"` + ServerCertInfo []*ServerCertInfo `json:"ServerCertInfo,omitnil,omitempty" name:"ServerCertInfo"` + UpstreamCertInfo *UpstreamCertInfo `json:"UpstreamCertInfo,omitnil,omitempty" name:"UpstreamCertInfo"` +} diff --git a/pkg/sdk3rd/tencentcloud/teomakers/api_describe_pages_zone_custom_domains.go b/pkg/sdk3rd/tencentcloud/teomakers/api_describe_pages_zone_custom_domains.go new file mode 100644 index 000000000..2ce17bf0c --- /dev/null +++ b/pkg/sdk3rd/tencentcloud/teomakers/api_describe_pages_zone_custom_domains.go @@ -0,0 +1,45 @@ +package teomakers + +import ( + "context" + "net/http" +) + +type DescribePagesZoneCustomDomainsRequest struct { + sdkRequestBase + + ProjectId *string `json:"ProjectId,omitempty"` +} + +type DescribePagesZoneCustomDomainsResponse struct { + sdkResponseBase + + Data *struct { + Response *struct { + RequestId *string `json:"RequestId,omitempty"` + TotalCount *int64 `json:"TotalCount,omitempty"` + PagesDomains []*PagesZoneCustomDomain `json:"PagesDomains,omitempty"` + } `json:"Response,omitempty"` + } `json:"Data,omitempty"` +} + +func (c *Client) DescribePagesZoneCustomDomains(req *DescribePagesZoneCustomDomainsRequest) (*DescribePagesZoneCustomDomainsResponse, error) { + return c.DescribePagesZoneCustomDomainsWithContext(context.Background(), req) +} + +func (c *Client) DescribePagesZoneCustomDomainsWithContext(ctx context.Context, req *DescribePagesZoneCustomDomainsRequest) (*DescribePagesZoneCustomDomainsResponse, error) { + httpreq, err := c.newRequest(http.MethodPost, "/", "DescribePagesZoneCustomDomains", req) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &DescribePagesZoneCustomDomainsResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/tencentcloud/teomakers/client.go b/pkg/sdk3rd/tencentcloud/teomakers/client.go new file mode 100644 index 000000000..3b2b666e1 --- /dev/null +++ b/pkg/sdk3rd/tencentcloud/teomakers/client.go @@ -0,0 +1,109 @@ +// A simple SDK client for Tencent Cloud EdgeOne Pages. +// API documentation: https://docs.edgeone.site/ +package teomakers + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/certimate-go/certimate/internal/app" +) + +type Client struct { + rc *resty.Client +} + +func NewClient(optFns ...OptionsFunc) (*Client, error) { + options := &Options{} + for _, fn := range optFns { + fn(options) + } + + if options.ApiToken == "" { + return nil, fmt.Errorf("sdkerr: unset apiToken") + } + + httper := resty.New(). + SetBaseURL("https://pages-api.cloud.tencent.com/v1"). + SetHeader("Accept", "application/json"). + SetHeader("Authorization", "Bearer "+options.ApiToken). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", app.AppUserAgent) + + return &Client{rc: httper}, nil +} + +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.rc.SetTimeout(timeout) + return c +} + +func (c *Client) newRequest(method string, path string, action string, params sdkRequest) (*resty.Request, error) { + if method == "" { + return nil, fmt.Errorf("sdkerr: unset method") + } + if path == "" { + return nil, fmt.Errorf("sdkerr: unset path") + } + if action == "" { + return nil, fmt.Errorf("sdkerr: unset action") + } + + req := c.rc.R() + req.Method = method + req.URL = path + + if params != nil { + params.SetAction(action) + req.SetBody(params) + } + + return req, nil +} + +func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + // WARN: + // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := c.doRequest(req) + if err != nil { + if resp != nil { + json.Unmarshal(resp.Body(), &res) + } + return resp, err + } + + if len(resp.Body()) != 0 { + if err := json.Unmarshal(resp.Body(), &res); err != nil { + return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) + } else { + if rCode := res.GetCode(); rCode != 0 { + return resp, fmt.Errorf("sdkerr: api error: code='%d', message='%s'", rCode, res.GetMessage()) + } + } + } + + return resp, nil +} diff --git a/pkg/sdk3rd/tencentcloud/teomakers/models.go b/pkg/sdk3rd/tencentcloud/teomakers/models.go new file mode 100644 index 000000000..900b7fb9e --- /dev/null +++ b/pkg/sdk3rd/tencentcloud/teomakers/models.go @@ -0,0 +1,26 @@ +package teomakers + +import ( + teo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901" +) + +type ( + OwnershipVerification = teo.OwnershipVerification + DnsVerification = teo.DnsVerification + FileVerification = teo.FileVerification + NsVerification = teo.NsVerification +) + +type PagesZoneCustomDomain struct { + Type *string `json:"Type,omitempty"` + Domain *string `json:"Domain,omitempty"` + ForceRedirectHTTPS *string `json:"ForceRedirectHTTPS,omitempty"` + RedirectStatusCode *int32 `json:"RedirectStatusCode,omitempty"` + CurrentCname *string `json:"CurrentCname,omitempty"` + MainDomain *string `json:"MainDomain,omitempty"` + Status *string `json:"Status,omitempty"` + OwnershipVerification *OwnershipVerification `json:"OwnershipVerification,omitempty"` + Cname *string `json:"Cname,omitempty"` + Area *string `json:"Area,omitempty"` + ZoneId *string `json:"ZoneId,omitempty"` +} diff --git a/pkg/sdk3rd/tencentcloud/teomakers/options.go b/pkg/sdk3rd/tencentcloud/teomakers/options.go new file mode 100644 index 000000000..bf8811871 --- /dev/null +++ b/pkg/sdk3rd/tencentcloud/teomakers/options.go @@ -0,0 +1,13 @@ +package teomakers + +type Options struct { + ApiToken string +} + +type OptionsFunc func(*Options) + +func WithApiToken(apiToken string) OptionsFunc { + return func(o *Options) { + o.ApiToken = apiToken + } +} diff --git a/pkg/sdk3rd/tencentcloud/teomakers/types.go b/pkg/sdk3rd/tencentcloud/teomakers/types.go new file mode 100644 index 000000000..6ae7e2f3b --- /dev/null +++ b/pkg/sdk3rd/tencentcloud/teomakers/types.go @@ -0,0 +1,44 @@ +package teomakers + +type sdkRequest interface { + SetAction(action string) +} + +type sdkRequestBase struct { + Action *string `json:"Action,omitempty"` +} + +func (r *sdkRequestBase) SetAction(action string) { + r.Action = &action +} + +var _ sdkRequest = (*sdkRequestBase)(nil) + +type sdkResponse interface { + GetCode() int + GetMessage() string +} + +type sdkResponseBase struct { + Code *int `json:"Code,omitempty"` + Message *string `json:"Message,omitempty"` + RequestId *string `json:"RequestId,omitempty"` +} + +func (r *sdkResponseBase) GetCode() int { + if r.Code == nil { + return 0 + } + + return *r.Code +} + +func (r *sdkResponseBase) GetMessage() string { + if r.Message == nil { + return "" + } + + return *r.Message +} + +var _ sdkResponse = (*sdkResponseBase)(nil) diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx index 8965de4cb..618095fb4 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx @@ -110,6 +110,7 @@ import BizDeployNodeConfigFieldsProviderTencentCloudCOS from "./BizDeployNodeCon import BizDeployNodeConfigFieldsProviderTencentCloudCSS from "./BizDeployNodeConfigFieldsProviderTencentCloudCSS"; import BizDeployNodeConfigFieldsProviderTencentCloudECDN from "./BizDeployNodeConfigFieldsProviderTencentCloudECDN"; import BizDeployNodeConfigFieldsProviderTencentCloudEO from "./BizDeployNodeConfigFieldsProviderTencentCloudEO"; +import BizDeployNodeConfigFieldsProviderTencentCloudEOMakers from "./BizDeployNodeConfigFieldsProviderTencentCloudEOMakers"; import BizDeployNodeConfigFieldsProviderTencentCloudGA2 from "./BizDeployNodeConfigFieldsProviderTencentCloudGA2"; import BizDeployNodeConfigFieldsProviderTencentCloudGAAP from "./BizDeployNodeConfigFieldsProviderTencentCloudGAAP"; import BizDeployNodeConfigFieldsProviderTencentCloudSCF from "./BizDeployNodeConfigFieldsProviderTencentCloudSCF"; @@ -259,6 +260,7 @@ const providerComponentMap: Partial { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const formInst = Form.useFormInstance(); + const initialValues = getInitialValues(); + + const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); + + return ( + <> + } + > + + + + } + > + + + + } + > + + + + + ) : ( + void 0 + ) + } + rules={[formRule]} + > + ({ + label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), + value: s, + }))} + /> + + + + + + + + + + + + + + + {t("workflow_node.deploy.form.tencentcloud_eomakers_enable_multiple_ssl.switch.suffix")} + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + apiToken: "", + projectId: "", + domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, + domains: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { + const { t } = i18n; + + return z + .object({ + endpoint: z.string().nullish(), + apiToken: z.string().nonempty(), + projectId: z.string().nonempty(), + domainMatchPattern: z.string().nonempty().default(DOMAIN_MATCH_PATTERN_EXACT), + domains: z.string().nullish(), + enableMultipleSSL: z.boolean().nullish(), + }) + .superRefine((values, ctx) => { + if (values.domainMatchPattern) { + switch (values.domainMatchPattern) { + case DOMAIN_MATCH_PATTERN_EXACT: + case DOMAIN_MATCH_PATTERN_WILDCARD: + { + const scDomains = z + .string() + .nonempty() + .refine((v) => v.split(MULTIPLE_INPUT_SEPARATOR).every((e) => isDomain(e, { allowWildcard: true })), t("common.errmsg.domain_invalid")); + const spDomains = scDomains.safeParse(values.domains); + if (!spDomains.success) { + ctx.addIssue({ + code: "custom", + message: z.treeifyError(spDomains.error).errors.join(), + path: ["domains"], + }); + } + } + break; + } + } + }); +}; + +const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudEOMakers, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 0804a3d1c..f3b247028 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -719,6 +719,7 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({ TENCENTCLOUD_CSS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-css`, TENCENTCLOUD_ECDN: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ecdn`, TENCENTCLOUD_EO: `${ACCESS_PROVIDERS.TENCENTCLOUD}-eo`, + TENCENTCLOUD_EOMAKERS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-eomakers`, TENCENTCLOUD_GAAP: `${ACCESS_PROVIDERS.TENCENTCLOUD}-gaap`, TENCENTCLOUD_GA2: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ga2`, TENCENTCLOUD_SCF: `${ACCESS_PROVIDERS.TENCENTCLOUD}-scf`, @@ -814,6 +815,7 @@ export const deploymentProvidersMap: Map
  • teo.intl.tencentcloudapi.com for Tencent Cloud International
  • teo.tencentcloudapi.com for Tencent Cloud in China
  • " + }, + "tencentcloud_eomakers_api_token": { + "label": "Tencent Cloud EdgeOne Makers API Token", + "placeholder": "Please enter Tencent Cloud EdgeOne Makers API Token", + "tooltip": "For more information, see https://pages.edgeone.ai/document/api-token" + }, + "tencentcloud_eomakers_project_id": { + "label": "Tencent Cloud EdgeOne Makers project ID", + "placeholder": "Please enter Tencent Cloud EdgeOne Makers project ID", + "tooltip": "For more information, see https://console.tencentcloud.com/edgeone/makers" + }, + "tencentcloud_eomakers_domains": { + "label": "Tencent Cloud EdgeOne Makers custom domains", + "placeholder": "Please enter Tencent Cloud EdgeOne Makers custom domain names (separated by semicolons)", + "help": "Notes: Multiple domains should be separated by semicolons.", + "multiple_input_modal": { + "title": "Change Tencent Cloud EdgeOne Makers custom domain", + "placeholder": "Please enter Tencent Cloud EdgeOne Makers custom domain name" + } + }, + "tencentcloud_eomakers_enable_multiple_ssl": { + "label": "Multiple SSL certificates", + "help": "Notes: Each domain name supports one RSA certificate and one ECC certificate at most.", + "switch": { + "suffix": "to retain other certificates with different algorithms.", + "on": "Allow", + "off": "Disallow" + } + }, "tencentcloud_ga2_endpoint": { "label": "Tencent Cloud API endpoint (Optional)", "placeholder": "Please enter Tencent Cloud GA2 API endpoint (e.g. ga2.intl.tencentcloudapi.com)", diff --git a/ui/src/i18n/resources/zh/nls.provider.json b/ui/src/i18n/resources/zh/nls.provider.json index 8ef6decab..70baabbd8 100644 --- a/ui/src/i18n/resources/zh/nls.provider.json +++ b/ui/src/i18n/resources/zh/nls.provider.json @@ -251,6 +251,7 @@ "tencentcloud_dns": "腾讯云 - 云解析 DNS", "tencentcloud_ecdn": "腾讯云 - 全站加速网络 ECDN", "tencentcloud_eo": "腾讯云 - 边缘安全加速平台 EdgeOne", + "tencentcloud_eomakers": "腾讯云 - 边缘安全加速平台 EdgeOne Makers(原 EdgeOne Pages)", "tencentcloud_ga2": "腾讯云 - 全球加速 2.0 GA2", "tencentcloud_gaap": "腾讯云 - 全球应用加速 GAAP", "tencentcloud_scf": "腾讯云 - 云函数 SCF", diff --git a/ui/src/i18n/resources/zh/nls.workflow.nodes.json b/ui/src/i18n/resources/zh/nls.workflow.nodes.json index b82fab89f..d43d3c1bb 100644 --- a/ui/src/i18n/resources/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/resources/zh/nls.workflow.nodes.json @@ -2317,6 +2317,39 @@ "off": "不" } }, + "tencentcloud_eomakers_endpoint": { + "label": "腾讯云接口端点(可选)", + "placeholder": "请输入腾讯云 EdgeOne 接口端点(例如:teo.tencentcloudapi.com)", + "tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/1552/80723
    国际站用户请填写 teo.intl.tencentcloudapi.com。" + }, + "tencentcloud_eomakers_api_token": { + "label": "腾讯云 EdgeOne Makers API Token", + "placeholder": "请输入腾讯云 EdgeOne Makers API Token", + "tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/1552/127422" + }, + "tencentcloud_eomakers_project_id": { + "label": "腾讯云 EdgeOne Makers 项目 ID", + "placeholder": "请输入腾讯云 EdgeOne Makers 项目 ID", + "tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/edgeone/makers" + }, + "tencentcloud_eomakers_domains": { + "label": "腾讯云 EdgeOne Makers 自定义域名", + "placeholder": "请输入腾讯云 EdgeOne Makers 自定义域名(多个值请用半角分号隔开)", + "help": "提示:支持多个域名,以半角分号隔开。", + "multiple_input_modal": { + "title": "修改腾讯云 EdgeOne Makers 自定义域名", + "placeholder": "请输入腾讯云 EdgeOne Makers 自定义域名" + } + }, + "tencentcloud_eomakers_enable_multiple_ssl": { + "label": "多证书模式", + "help": "提示:每个域名最多支持一个 RSA 证书、一个 ECC 证书。", + "switch": { + "suffix": "保留与待部署证书算法不一致的其他有效证书。", + "on": "", + "off": "不" + } + }, "tencentcloud_ga2_endpoint": { "label": "腾讯云接口端点(可选)", "placeholder": "请输入腾讯云 GA2 接口端点(例如:ga2.tencentcloudapi.com)",