Compare commits

...

15 Commits

Author SHA1 Message Date
Yoan.liu
9a06c1e35b
Merge pull request #558 from fudiwei/main
new providers & bugfix
2025-03-26 17:59:27 +08:00
Fu Diwei
382de0d6d6 refactor: clean code 2025-03-26 10:43:37 +08:00
Fu Diwei
4883b3bb88 feat(ui): make request error friendly 2025-03-26 10:18:27 +08:00
Fu Diwei
0a90523d61 fix(ui): login theme error in dark mode 2025-03-25 20:31:02 +08:00
Fu Diwei
fa63f2a838 feat: add tencentcloud-eo dns-01 applicant 2025-03-25 20:28:05 +08:00
Fu Diwei
fd8ac3ae37 feat(ui): allow select dns-01 provider on application 2025-03-25 19:52:09 +08:00
Fu Diwei
51c1b193e5 fix: #559 2025-03-25 19:41:55 +08:00
Fu Diwei
e9610eaede fix: #556 2025-03-25 16:17:35 +08:00
Fu Diwei
7d5c714211 feat: improve i18n 2025-03-25 13:54:00 +08:00
Fu Diwei
24e275fdb3 feat: add volcengine certcenter deployer 2025-03-25 13:54:00 +08:00
Fu Diwei
597b9d0e17 feat: add huaweicloud scm deployer 2025-03-25 13:54:00 +08:00
Fu Diwei
4d710a1aaf feat: add baiducloud cert deployer 2025-03-25 13:54:00 +08:00
Fu Diwei
5de033814b feat: add baiducloud appblb deployer 2025-03-25 13:54:00 +08:00
Fu Diwei
aaec840d8c feat: add baiducloud blb deployer 2025-03-25 13:53:54 +08:00
Fu Diwei
e579cf6ceb feat: add baiducloud cert uploader 2025-03-25 13:53:39 +08:00
53 changed files with 2637 additions and 181 deletions

View File

@ -38,8 +38,8 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
- 灵活的工作流编排方式,证书从申请到部署完全自动化;
- 支持单域名、多域名、泛域名证书,可选 RSA、ECC 签名算法;
- 支持 PEM、PFX、JKS 等多种格式输出证书;
- 支持 20+ 域名托管商如阿里云、腾讯云、Cloudflare 等,[点此查看](https://docs.certimate.me/docs/reference/providers#supported-dns-providers)完整提供商清单
- 支持 60+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等[点此查看](https://docs.certimate.me/docs/reference/providers#supported-host-providers)完整提供商清单
- 支持 20+ 域名托管商如阿里云、腾讯云、Cloudflare 等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-dns-providers)
- 支持 70+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-host-providers)
- 支持邮件、钉钉、飞书、企业微信、Webhook 等多种通知渠道;
- 支持 Let's Encrypt、ZeroSSL、Google Trust Services 等多种 ACME 证书颁发机构;
- 更多特性等待探索。

View File

@ -39,7 +39,7 @@ Certimate aims to provide users with a secure and user-friendly SSL certificate
- Supports single-domain, multi-domain, wildcard certificates, with options for RSA or ECC.
- Supports various certificate formats such as PEM, PFX, JKS.
- Supports more than 20+ domain registrars (e.g., Alibaba Cloud, Tencent Cloud, Cloudflare, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-dns-providers));
- Supports more than 60+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-host-providers));
- Supports more than 70+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-host-providers));
- Supports multiple notification channels including email, DingTalk, Feishu, WeCom, Webhook, and more;
- Supports multiple ACME CAs including Let's Encrypt, ZeroSSL, Google Trust Services, and more;
- More features waiting to be discovered.

View File

@ -73,12 +73,7 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
} else {
accessConfig, err := access.UnmarshalConfigToMap()
if err != nil {
return nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
options.ProviderAccessConfig = accessConfig
options.ProviderAccessConfig = access.Config
}
certRepo := repository.NewCertificateRepository()

View File

@ -30,6 +30,7 @@ import (
pPowerDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns"
pRainYun "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/rainyun"
pTencentCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud"
pTencentCloudEO "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo"
pVercel "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/vercel"
pVolcEngine "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/volcengine"
pWestcn "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/westcn"
@ -294,7 +295,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
applicant, err := pJDCloud.NewChallengeProvider(&pJDCloud.ChallengeProviderConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
RegionId: maputil.GetString(options.ProviderApplyConfig, "region_id"),
RegionId: maputil.GetString(options.ProviderApplyConfig, "regionId"),
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
@ -410,20 +411,36 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS:
case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS, domain.ApplyDNSProviderTypeTencentCloudEO:
{
access := domain.AccessConfigForTencentCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pTencentCloud.NewChallengeProvider(&pTencentCloud.ChallengeProviderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
switch options.Provider {
case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS:
applicant, err := pTencentCloud.NewChallengeProvider(&pTencentCloud.ChallengeProviderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
case domain.ApplyDNSProviderTypeTencentCloudEO:
applicant, err := pTencentCloudEO.NewChallengeProvider(&pTencentCloudEO.ChallengeProviderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
ZoneId: maputil.GetString(options.ProviderApplyConfig, "zoneId"),
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
default:
break
}
}
case domain.ApplyDNSProviderTypeVercel:

View File

@ -39,14 +39,9 @@ func NewWithDeployNode(node *domain.WorkflowNode, certdata struct {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
}
accessConfig, err := access.UnmarshalConfigToMap()
if err != nil {
return nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
deployer, err := createDeployer(&deployerOptions{
Provider: domain.DeployProviderType(nodeConfig.Provider),
ProviderAccessConfig: accessConfig,
ProviderAccessConfig: access.Config,
ProviderDeployConfig: nodeConfig.ProviderConfig,
})
if err != nil {

View File

@ -24,7 +24,10 @@ import (
pAWSACM "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aws-acm"
pAWSCloudFront "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aws-cloudfront"
pAzureKeyVault "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/azure-keyvault"
pBaiduCloudAppBLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-appblb"
pBaiduCloudBLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-blb"
pBaiduCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cdn"
pBaiduCloudCert "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cert"
pBaishanCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baishan-cdn"
pBaotaPanelConsole "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotapanel-console"
pBaotaPanelSite "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotapanel-site"
@ -36,6 +39,7 @@ import (
pGcoreCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/gcore-cdn"
pHuaweiCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-cdn"
pHuaweiCloudELB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-elb"
pHuaweiCloudSCM "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-scm"
pHuaweiCloudWAF "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-waf"
pJDCloudALB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/jdcloud-alb"
pJDCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/jdcloud-cdn"
@ -63,6 +67,7 @@ import (
pUpyunCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/upyun-cdn"
pVolcEngineALB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-alb"
pVolcEngineCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-cdn"
pVolcEngineCertCenter "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-certcenter"
pVolcEngineCLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-clb"
pVolcEngineDCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-dcdn"
pVolcEngineImageX "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-imagex"
@ -303,7 +308,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
}
}
case domain.DeployProviderTypeBaiduCloudCDN:
case domain.DeployProviderTypeBaiduCloudAppBLB, domain.DeployProviderTypeBaiduCloudBLB, domain.DeployProviderTypeBaiduCloudCDN, domain.DeployProviderTypeBaiduCloudCert:
{
access := domain.AccessConfigForBaiduCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -311,6 +316,30 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
}
switch options.Provider {
case domain.DeployProviderTypeBaiduCloudAppBLB:
deployer, err := pBaiduCloudAppBLB.NewDeployer(&pBaiduCloudAppBLB.DeployerConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Region: maputil.GetString(options.ProviderDeployConfig, "region"),
ResourceType: pBaiduCloudAppBLB.ResourceType(maputil.GetString(options.ProviderDeployConfig, "resourceType")),
LoadbalancerId: maputil.GetString(options.ProviderDeployConfig, "loadbalancerId"),
ListenerPort: maputil.GetInt32(options.ProviderDeployConfig, "listenerPort"),
Domain: maputil.GetString(options.ProviderDeployConfig, "domain"),
})
return deployer, err
case domain.DeployProviderTypeBaiduCloudBLB:
deployer, err := pBaiduCloudBLB.NewDeployer(&pBaiduCloudBLB.DeployerConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Region: maputil.GetString(options.ProviderDeployConfig, "region"),
ResourceType: pBaiduCloudBLB.ResourceType(maputil.GetString(options.ProviderDeployConfig, "resourceType")),
LoadbalancerId: maputil.GetString(options.ProviderDeployConfig, "loadbalancerId"),
ListenerPort: maputil.GetInt32(options.ProviderDeployConfig, "listenerPort"),
Domain: maputil.GetString(options.ProviderDeployConfig, "domain"),
})
return deployer, err
case domain.DeployProviderTypeBaiduCloudCDN:
deployer, err := pBaiduCloudCDN.NewDeployer(&pBaiduCloudCDN.DeployerConfig{
AccessKeyId: access.AccessKeyId,
@ -319,6 +348,13 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
})
return deployer, err
case domain.DeployProviderTypeBaiduCloudCert:
deployer, err := pBaiduCloudCert.NewDeployer(&pBaiduCloudCert.DeployerConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
})
return deployer, err
default:
break
}
@ -479,7 +515,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
}
}
case domain.DeployProviderTypeHuaweiCloudCDN, domain.DeployProviderTypeHuaweiCloudELB, domain.DeployProviderTypeHuaweiCloudWAF:
case domain.DeployProviderTypeHuaweiCloudCDN, domain.DeployProviderTypeHuaweiCloudELB, domain.DeployProviderTypeHuaweiCloudSCM, domain.DeployProviderTypeHuaweiCloudWAF:
{
access := domain.AccessConfigForHuaweiCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -508,6 +544,13 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
})
return deployer, err
case domain.DeployProviderTypeHuaweiCloudSCM:
deployer, err := pHuaweiCloudSCM.NewDeployer(&pHuaweiCloudSCM.DeployerConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
})
return deployer, err
case domain.DeployProviderTypeHuaweiCloudWAF:
deployer, err := pHuaweiCloudWAF.NewDeployer(&pHuaweiCloudWAF.DeployerConfig{
AccessKeyId: access.AccessKeyId,
@ -849,7 +892,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
}
}
case domain.DeployProviderTypeVolcEngineALB, domain.DeployProviderTypeVolcEngineCDN, domain.DeployProviderTypeVolcEngineCLB, domain.DeployProviderTypeVolcEngineDCDN, domain.DeployProviderTypeVolcEngineImageX, domain.DeployProviderTypeVolcEngineLive, domain.DeployProviderTypeVolcEngineTOS:
case domain.DeployProviderTypeVolcEngineALB, domain.DeployProviderTypeVolcEngineCDN, domain.DeployProviderTypeVolcEngineCertCenter, domain.DeployProviderTypeVolcEngineCLB, domain.DeployProviderTypeVolcEngineDCDN, domain.DeployProviderTypeVolcEngineImageX, domain.DeployProviderTypeVolcEngineLive, domain.DeployProviderTypeVolcEngineTOS:
{
access := domain.AccessConfigForVolcEngine{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -877,6 +920,14 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
})
return deployer, err
case domain.DeployProviderTypeVolcEngineCertCenter:
deployer, err := pVolcEngineCertCenter.NewDeployer(&pVolcEngineCertCenter.DeployerConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.SecretAccessKey,
Region: maputil.GetString(options.ProviderDeployConfig, "region"),
})
return deployer, err
case domain.DeployProviderTypeVolcEngineCLB:
deployer, err := pVolcEngineCLB.NewDeployer(&pVolcEngineCLB.DeployerConfig{
AccessKeyId: access.AccessKeyId,

View File

@ -1,7 +1,6 @@
package domain
import (
"encoding/json"
"time"
)
@ -9,19 +8,10 @@ const CollectionNameAccess = "access"
type Access struct {
Meta
Name string `json:"name" db:"name"`
Provider string `json:"provider" db:"provider"`
Config string `json:"config" db:"config"`
DeletedAt *time.Time `json:"deleted" db:"deleted"`
}
func (a *Access) UnmarshalConfigToMap() (map[string]any, error) {
config := make(map[string]any)
if err := json.Unmarshal([]byte(a.Config), &config); err != nil {
return nil, err
}
return config, nil
Name string `json:"name" db:"name"`
Provider string `json:"provider" db:"provider"`
Config map[string]any `json:"config" db:"config"`
DeletedAt *time.Time `json:"deleted" db:"deleted"`
}
type AccessConfigFor1Panel struct {

View File

@ -101,6 +101,7 @@ const (
ApplyDNSProviderTypeRainYun = ApplyDNSProviderType("rainyun")
ApplyDNSProviderTypeTencentCloud = ApplyDNSProviderType("tencentcloud") // 兼容旧值,等同于 [ApplyDNSProviderTypeTencentCloudDNS]
ApplyDNSProviderTypeTencentCloudDNS = ApplyDNSProviderType("tencentcloud-dns")
ApplyDNSProviderTypeTencentCloudEO = ApplyDNSProviderType("tencentcloud-eo")
ApplyDNSProviderTypeVercel = ApplyDNSProviderType("vercel")
ApplyDNSProviderTypeVolcEngine = ApplyDNSProviderType("volcengine") // 兼容旧值,等同于 [ApplyDNSProviderTypeVolcEngineDNS]
ApplyDNSProviderTypeVolcEngineDNS = ApplyDNSProviderType("volcengine-dns")
@ -135,7 +136,10 @@ const (
DeployProviderTypeAWSACM = DeployProviderType("aws-acm")
DeployProviderTypeAWSCloudFront = DeployProviderType("aws-cloudfront")
DeployProviderTypeAzureKeyVault = DeployProviderType("azure-keyvault")
DeployProviderTypeBaiduCloudAppBLB = DeployProviderType("baiducloud-appblb")
DeployProviderTypeBaiduCloudBLB = DeployProviderType("baiducloud-blb")
DeployProviderTypeBaiduCloudCDN = DeployProviderType("baiducloud-cdn")
DeployProviderTypeBaiduCloudCert = DeployProviderType("baiducloud-cert")
DeployProviderTypeBaishanCDN = DeployProviderType("baishan-cdn")
DeployProviderTypeBaotaPanelConsole = DeployProviderType("baotapanel-console")
DeployProviderTypeBaotaPanelSite = DeployProviderType("baotapanel-site")
@ -147,6 +151,7 @@ const (
DeployProviderTypeGcoreCDN = DeployProviderType("gcore-cdn")
DeployProviderTypeHuaweiCloudCDN = DeployProviderType("huaweicloud-cdn")
DeployProviderTypeHuaweiCloudELB = DeployProviderType("huaweicloud-elb")
DeployProviderTypeHuaweiCloudSCM = DeployProviderType("huaweicloud-scm")
DeployProviderTypeHuaweiCloudWAF = DeployProviderType("huaweicloud-waf")
DeployProviderTypeJDCloudALB = DeployProviderType("jdcloud-alb")
DeployProviderTypeJDCloudCDN = DeployProviderType("jdcloud-cdn")
@ -176,6 +181,7 @@ const (
DeployProviderTypeUpyunFile = DeployProviderType("upyun-file")
DeployProviderTypeVolcEngineALB = DeployProviderType("volcengine-alb")
DeployProviderTypeVolcEngineCDN = DeployProviderType("volcengine-cdn")
DeployProviderTypeVolcEngineCertCenter = DeployProviderType("volcengine-certcenter")
DeployProviderTypeVolcEngineCLB = DeployProviderType("volcengine-clb")
DeployProviderTypeVolcEngineDCDN = DeployProviderType("volcengine-dcdn")
DeployProviderTypeVolcEngineImageX = DeployProviderType("volcengine-imagex")

View File

@ -106,6 +106,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
if err != nil {
return err
}
if record == nil {
// add new record
resp, err := d.client.CreateRecordOpenapi(&model.CreateRecordOpenapiRequest{

View File

@ -0,0 +1,207 @@
package lego_tencentcloudeo
import (
"errors"
"fmt"
"math"
"strings"
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
teo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901"
)
const (
envNamespace = "TENCENTCLOUDEO_"
EnvSecretID = envNamespace + "SECRET_ID"
EnvSecretKey = envNamespace + "SECRET_KEY"
EnvZoneId = envNamespace + "ZONE_ID"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
type Config struct {
SecretID string
SecretKey string
ZoneId string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int32
HTTPTimeout time.Duration
}
type DNSProvider struct {
client *teo.Client
config *Config
}
func NewDefaultConfig() *Config {
return &Config{
TTL: int32(env.GetOrDefaultInt(EnvTTL, 300)),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
}
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvSecretID, EnvSecretKey, EnvZoneId)
if err != nil {
return nil, fmt.Errorf("tencentcloud-eo: %w", err)
}
config := NewDefaultConfig()
config.SecretID = values[EnvSecretID]
config.SecretKey = values[EnvSecretKey]
config.ZoneId = values[EnvSecretKey]
return NewDNSProviderConfig(config)
}
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("tencentcloud-eo: the configuration of the DNS provider is nil")
}
credential := common.NewCredential(config.SecretID, config.SecretKey)
cpf := profile.NewClientProfile()
cpf.HttpProfile.ReqTimeout = int(math.Round(config.HTTPTimeout.Seconds()))
client, err := teo.NewClient(credential, "", cpf)
if err != nil {
return nil, err
}
return &DNSProvider{
client: client,
config: config,
}, nil
}
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
if err := d.addOrUpdateDNSRecord(strings.TrimRight(info.EffectiveFQDN, "."), info.Value); err != nil {
return fmt.Errorf("tencentcloud-eo: %w", err)
}
return nil
}
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
if err := d.removeDNSRecord(strings.TrimRight(info.EffectiveFQDN, ".")); err != nil {
return fmt.Errorf("tencentcloud-eo: %w", err)
}
return nil
}
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) getDNSRecord(effectiveFQDN string) (*teo.DnsRecord, error) {
pageOffset := 0
pageLimit := 1000
for {
request := teo.NewDescribeDnsRecordsRequest()
request.ZoneId = common.StringPtr(d.config.ZoneId)
request.Offset = common.Int64Ptr(int64(pageOffset))
request.Limit = common.Int64Ptr(int64(pageLimit))
request.Filters = []*teo.AdvancedFilter{
{
Name: common.StringPtr("type"),
Values: []*string{common.StringPtr("TXT")},
},
}
response, err := d.client.DescribeDnsRecords(request)
if err != nil {
return nil, err
}
if response.Response == nil {
break
} else {
for _, record := range response.Response.DnsRecords {
if *record.Name == effectiveFQDN {
return record, nil
}
}
if len(response.Response.DnsRecords) < int(pageLimit) {
break
}
pageOffset += len(response.Response.DnsRecords)
}
}
return nil, nil
}
func (d *DNSProvider) addOrUpdateDNSRecord(effectiveFQDN, value string) error {
record, err := d.getDNSRecord(effectiveFQDN)
if err != nil {
return err
}
if record == nil {
request := teo.NewCreateDnsRecordRequest()
request.ZoneId = common.StringPtr(d.config.ZoneId)
request.Name = common.StringPtr(effectiveFQDN)
request.Type = common.StringPtr("TXT")
request.Content = common.StringPtr(value)
request.TTL = common.Int64Ptr(int64(d.config.TTL))
_, err := d.client.CreateDnsRecord(request)
return err
} else {
record.Content = common.StringPtr(value)
request := teo.NewModifyDnsRecordsRequest()
request.ZoneId = common.StringPtr(d.config.ZoneId)
request.DnsRecords = []*teo.DnsRecord{record}
if _, err := d.client.ModifyDnsRecords(request); err != nil {
return err
}
if *record.Status == "disable" {
request := teo.NewModifyDnsRecordsStatusRequest()
request.ZoneId = common.StringPtr(d.config.ZoneId)
request.RecordsToEnable = []*string{record.RecordId}
if _, err = d.client.ModifyDnsRecordsStatus(request); err != nil {
return err
}
}
return nil
}
}
func (d *DNSProvider) removeDNSRecord(effectiveFQDN string) error {
record, err := d.getDNSRecord(effectiveFQDN)
if err != nil {
return err
}
if record == nil {
return nil
} else {
request := teo.NewDeleteDnsRecordsRequest()
request.ZoneId = common.StringPtr(d.config.ZoneId)
request.RecordIds = []*string{record.RecordId}
_, err = d.client.DeleteDnsRecords(request)
return err
}
}

View File

@ -0,0 +1,41 @@
package tencentcloudeo
import (
"time"
"github.com/go-acme/lego/v4/challenge"
internal "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/internal"
)
type ChallengeProviderConfig struct {
SecretId string `json:"secretId"`
SecretKey string `json:"secretKey"`
ZoneId string `json:"zoneId"`
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
DnsTTL int32 `json:"dnsTTL,omitempty"`
}
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
if config == nil {
panic("config is nil")
}
providerConfig := internal.NewDefaultConfig()
providerConfig.SecretID = config.SecretId
providerConfig.SecretKey = config.SecretKey
providerConfig.ZoneId = config.ZoneId
if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
}
if config.DnsTTL != 0 {
providerConfig.TTL = config.DnsTTL
}
provider, err := internal.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@ -46,7 +46,7 @@ Shell command to run this test:
--CERTIMATE_DEPLOYER_ALIYUNCLB_REGION="cn-hangzhou" \
--CERTIMATE_DEPLOYER_ALIYUNCLB_LOADBALANCERID="your-clb-instance-id" \
--CERTIMATE_DEPLOYER_ALIYUNCLB_LISTENERPORT=443 \
--CERTIMATE_DEPLOYER_ALIYUNCLB_DOMAIN="your-alb-sni-domain"
--CERTIMATE_DEPLOYER_ALIYUNCLB_DOMAIN="your-clb-sni-domain"
*/
func TestDeploy(t *testing.T) {
flag.Parse()

View File

@ -15,6 +15,7 @@ import (
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas"
"github.com/usual2970/certimate/internal/pkg/utils/sliceutil"
)
type DeployerConfig struct {
@ -156,26 +157,10 @@ func (d *DeployerProvider) deployToWAF3(ctx context.Context, certPem string, pri
InstanceId: tea.String(d.config.InstanceId),
RegionId: tea.String(d.config.Region),
Domain: tea.String(d.config.Domain),
Listen: &aliwaf.ModifyDomainRequestListen{
CertId: tea.String(upres.CertId),
TLSVersion: tea.String("tlsv1"),
EnableTLSv3: tea.Bool(false),
},
Redirect: &aliwaf.ModifyDomainRequestRedirect{
Loadbalance: tea.String("iphash"),
},
}
if describeDomainDetailResp.Body != nil && describeDomainDetailResp.Body.Listen != nil {
modifyDomainReq.Listen.TLSVersion = describeDomainDetailResp.Body.Listen.TLSVersion
modifyDomainReq.Listen.EnableTLSv3 = describeDomainDetailResp.Body.Listen.EnableTLSv3
modifyDomainReq.Listen.FocusHttps = describeDomainDetailResp.Body.Listen.FocusHttps
}
if describeDomainDetailResp.Body != nil && describeDomainDetailResp.Body.Redirect != nil {
modifyDomainReq.Redirect.Loadbalance = describeDomainDetailResp.Body.Redirect.Loadbalance
modifyDomainReq.Redirect.FocusHttpBackend = describeDomainDetailResp.Body.Redirect.FocusHttpBackend
modifyDomainReq.Redirect.SniEnabled = describeDomainDetailResp.Body.Redirect.SniEnabled
modifyDomainReq.Redirect.SniHost = describeDomainDetailResp.Body.Redirect.SniHost
Listen: &aliwaf.ModifyDomainRequestListen{CertId: tea.String(upres.CertId)},
Redirect: &aliwaf.ModifyDomainRequestRedirect{Loadbalance: tea.String("iphash")},
}
modifyDomainReq = assign(modifyDomainReq, describeDomainDetailResp.Body)
modifyDomainResp, err := d.sdkClient.ModifyDomain(modifyDomainReq)
d.logger.Debug("sdk request 'waf.ModifyDomain'", slog.Any("request", modifyDomainReq), slog.Any("response", modifyDomainResp))
if err != nil {
@ -222,3 +207,166 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up
})
return uploader, err
}
func assign(source *aliwaf.ModifyDomainRequest, target *aliwaf.DescribeDomainDetailResponseBody) *aliwaf.ModifyDomainRequest {
// `ModifyDomain` 中不传的字段表示使用默认值、而非保留原值,
// 因此这里需要把原配置中的参数重新赋值回去。
if target == nil {
return source
}
if target.Listen != nil {
if source.Listen == nil {
source.Listen = &aliwaf.ModifyDomainRequestListen{}
}
if target.Listen.CipherSuite != nil {
source.Listen.CipherSuite = tea.Int32(int32(*target.Listen.CipherSuite))
}
if target.Listen.CustomCiphers != nil {
source.Listen.CustomCiphers = target.Listen.CustomCiphers
}
if target.Listen.EnableTLSv3 != nil {
source.Listen.EnableTLSv3 = target.Listen.EnableTLSv3
}
if target.Listen.ExclusiveIp != nil {
source.Listen.ExclusiveIp = target.Listen.ExclusiveIp
}
if target.Listen.FocusHttps != nil {
source.Listen.FocusHttps = target.Listen.FocusHttps
}
if target.Listen.Http2Enabled != nil {
source.Listen.Http2Enabled = target.Listen.Http2Enabled
}
if target.Listen.HttpPorts != nil {
source.Listen.HttpPorts = sliceutil.Map(target.Listen.HttpPorts, func(v *int64) *int32 {
if v == nil {
return nil
}
return tea.Int32(int32(*v))
})
}
if target.Listen.HttpsPorts != nil {
source.Listen.HttpsPorts = sliceutil.Map(target.Listen.HttpsPorts, func(v *int64) *int32 {
if v == nil {
return nil
}
return tea.Int32(int32(*v))
})
}
if target.Listen.IPv6Enabled != nil {
source.Listen.IPv6Enabled = target.Listen.IPv6Enabled
}
if target.Listen.ProtectionResource != nil {
source.Listen.ProtectionResource = target.Listen.ProtectionResource
}
if target.Listen.TLSVersion != nil {
source.Listen.TLSVersion = target.Listen.TLSVersion
}
if target.Listen.XffHeaderMode != nil {
source.Listen.XffHeaderMode = tea.Int32(int32(*target.Listen.XffHeaderMode))
}
if target.Listen.XffHeaders != nil {
source.Listen.XffHeaders = target.Listen.XffHeaders
}
}
if target.Redirect != nil {
if source.Redirect == nil {
source.Redirect = &aliwaf.ModifyDomainRequestRedirect{}
}
if target.Redirect.Backends != nil {
source.Redirect.Backends = sliceutil.Map(target.Redirect.Backends, func(v *aliwaf.DescribeDomainDetailResponseBodyRedirectBackends) *string {
if v == nil {
return nil
}
return v.Backend
})
}
if target.Redirect.BackupBackends != nil {
source.Redirect.BackupBackends = sliceutil.Map(target.Redirect.BackupBackends, func(v *aliwaf.DescribeDomainDetailResponseBodyRedirectBackupBackends) *string {
if v == nil {
return nil
}
return v.Backend
})
}
if target.Redirect.ConnectTimeout != nil {
source.Redirect.ConnectTimeout = target.Redirect.ConnectTimeout
}
if target.Redirect.FocusHttpBackend != nil {
source.Redirect.FocusHttpBackend = target.Redirect.FocusHttpBackend
}
if target.Redirect.Keepalive != nil {
source.Redirect.Keepalive = target.Redirect.Keepalive
}
if target.Redirect.KeepaliveRequests != nil {
source.Redirect.KeepaliveRequests = target.Redirect.KeepaliveRequests
}
if target.Redirect.KeepaliveTimeout != nil {
source.Redirect.KeepaliveTimeout = target.Redirect.KeepaliveTimeout
}
if target.Redirect.Loadbalance != nil {
source.Redirect.Loadbalance = target.Redirect.Loadbalance
}
if target.Redirect.ReadTimeout != nil {
source.Redirect.ReadTimeout = target.Redirect.ReadTimeout
}
if target.Redirect.RequestHeaders != nil {
source.Redirect.RequestHeaders = sliceutil.Map(target.Redirect.RequestHeaders, func(v *aliwaf.DescribeDomainDetailResponseBodyRedirectRequestHeaders) *aliwaf.ModifyDomainRequestRedirectRequestHeaders {
if v == nil {
return nil
}
return &aliwaf.ModifyDomainRequestRedirectRequestHeaders{
Key: v.Key,
Value: v.Value,
}
})
}
if target.Redirect.Retry != nil {
source.Redirect.Retry = target.Redirect.Retry
}
if target.Redirect.SniEnabled != nil {
source.Redirect.SniEnabled = target.Redirect.SniEnabled
}
if target.Redirect.SniHost != nil {
source.Redirect.SniHost = target.Redirect.SniHost
}
if target.Redirect.WriteTimeout != nil {
source.Redirect.WriteTimeout = target.Redirect.WriteTimeout
}
if target.Redirect.XffProto != nil {
source.Redirect.XffProto = target.Redirect.XffProto
}
}
return source
}

View File

@ -0,0 +1,332 @@
package baiducloudappblb
import (
"context"
"errors"
"fmt"
"log/slog"
"strconv"
"strings"
bceappblb "github.com/baidubce/bce-sdk-go/services/appblb"
"github.com/google/uuid"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cert"
"github.com/usual2970/certimate/internal/pkg/utils/sliceutil"
)
type DeployerConfig struct {
// 百度智能云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 百度智能云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 百度智能云区域。
Region string `json:"region"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 负载均衡实例 ID。
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听端口。
// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。
ListenerPort int32 `json:"listenerPort,omitempty"`
// SNI 域名(支持泛域名)。
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。
Domain string `json:"domain,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *bceappblb.Client
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 CAS
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_LOADBALANCER:
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
return nil, err
}
case RESOURCE_TYPE_LISTENER:
if err := d.deployToListener(ctx, upres.CertId); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
}
return &deployer.DeployResult{}, nil
}
func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
// 查询 BLB 实例详情
// REF: https://cloud.baidu.com/doc/BLB/s/6jwvxnyhi#describeloadbalancerdetail%E6%9F%A5%E8%AF%A2blb%E5%AE%9E%E4%BE%8B%E8%AF%A6%E6%83%85
describeLoadBalancerDetailResp, err := d.sdkClient.DescribeLoadBalancerDetail(d.config.LoadbalancerId)
d.logger.Debug("sdk request 'appblb.DescribeLoadBalancerAttribute'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("response", describeLoadBalancerDetailResp))
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'appblb.DescribeLoadBalancerDetail'")
}
// 获取全部 HTTPS/SSL 监听端口
listeners := make([]struct {
Type string
Port int32
}, 0)
for _, listener := range describeLoadBalancerDetailResp.Listener {
if listener.Type == "HTTPS" || listener.Type == "SSL" {
listenerPort, err := strconv.Atoi(listener.Port)
if err != nil {
continue
}
listeners = append(listeners, struct {
Type string
Port int32
}{
Type: listener.Type,
Port: int32(listenerPort),
})
}
}
// 遍历更新监听证书
if len(listeners) == 0 {
d.logger.Info("no blb listeners to deploy")
} else {
d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners))
var errs []error
for _, listener := range listeners {
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
if d.config.ListenerPort == 0 {
return errors.New("config `listenerPort` is required")
}
// 查询监听
// REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#describeappalllisteners%E6%9F%A5%E8%AF%A2%E6%89%80%E6%9C%89%E7%9B%91%E5%90%AC
describeAppAllListenersRequest := &bceappblb.DescribeAppListenerArgs{
ListenerPort: uint16(d.config.ListenerPort),
}
describeAppAllListenersResp, err := d.sdkClient.DescribeAppAllListeners(d.config.LoadbalancerId, describeAppAllListenersRequest)
d.logger.Debug("sdk request 'appblb.DescribeAppAllListeners'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("request", describeAppAllListenersRequest), slog.Any("response", describeAppAllListenersResp))
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'appblb.DescribeAppAllListeners'")
}
// 获取全部 HTTPS/SSL 监听端口
listeners := make([]struct {
Type string
Port int32
}, 0)
for _, listener := range describeAppAllListenersResp.ListenerList {
if listener.ListenerType == "HTTPS" || listener.ListenerType == "SSL" {
listeners = append(listeners, struct {
Type string
Port int32
}{
Type: listener.ListenerType,
Port: int32(listener.ListenerPort),
})
}
}
// 遍历更新监听证书
if len(listeners) == 0 {
d.logger.Info("no blb listeners to deploy")
} else {
d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners))
var errs []error
for _, listener := range listeners {
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *DeployerProvider) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerType string, cloudListenerPort int32, cloudCertId string) error {
switch strings.ToUpper(cloudListenerType) {
case "HTTPS":
return d.updateHttpsListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId)
case "SSL":
return d.updateSslListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId)
default:
return fmt.Errorf("unsupported listener type: %s", cloudListenerType)
}
}
func (d *DeployerProvider) updateHttpsListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error {
// 查询 HTTPS 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#describeapphttpslisteners%E6%9F%A5%E8%AF%A2https%E7%9B%91%E5%90%AC%E5%99%A8
describeAppHTTPSListenersReq := &bceappblb.DescribeAppListenerArgs{
ListenerPort: uint16(cloudHttpsListenerPort),
MaxKeys: 1,
}
describeAppHTTPSListenersResp, err := d.sdkClient.DescribeAppHTTPSListeners(cloudLoadbalancerId, describeAppHTTPSListenersReq)
d.logger.Debug("sdk request 'appblb.DescribeAppHTTPSListeners'", slog.String("blbId", cloudLoadbalancerId), slog.Any("request", describeAppHTTPSListenersReq), slog.Any("response", describeAppHTTPSListenersResp))
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'appblb.DescribeAppHTTPSListeners'")
} else if len(describeAppHTTPSListenersResp.ListenerList) == 0 {
return fmt.Errorf("listener %s:%d not found", cloudLoadbalancerId, cloudHttpsListenerPort)
}
if d.config.Domain == "" {
// 未指定 SNI只需部署到监听器
// 更新 HTTPS 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#updateapphttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8
updateAppHTTPSListenerReq := &bceappblb.UpdateAppHTTPSListenerArgs{
ClientToken: generateClientToken(),
ListenerPort: uint16(cloudHttpsListenerPort),
CertIds: []string{cloudCertId},
}
err := d.sdkClient.UpdateAppHTTPSListener(cloudLoadbalancerId, updateAppHTTPSListenerReq)
d.logger.Debug("sdk request 'appblb.UpdateAppHTTPSListener'", slog.Any("request", updateAppHTTPSListenerReq))
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'appblb.UpdateAppHTTPSListener'")
}
} else {
// 指定 SNI需部署到扩展域名
// 更新 HTTPS 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8
updateAppHTTPSListenerReq := &bceappblb.UpdateAppHTTPSListenerArgs{
ClientToken: generateClientToken(),
ListenerPort: uint16(cloudHttpsListenerPort),
AdditionalCertDomains: sliceutil.Map(describeAppHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceappblb.AdditionalCertDomainsModel) bceappblb.AdditionalCertDomainsModel {
if domain.Host == d.config.Domain {
return bceappblb.AdditionalCertDomainsModel{
Host: domain.Host,
CertId: cloudCertId,
}
}
return bceappblb.AdditionalCertDomainsModel{
Host: domain.Host,
CertId: domain.CertId,
}
}),
}
err := d.sdkClient.UpdateAppHTTPSListener(cloudLoadbalancerId, updateAppHTTPSListenerReq)
d.logger.Debug("sdk request 'appblb.UpdateAppHTTPSListener'", slog.Any("request", updateAppHTTPSListenerReq))
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'appblb.UpdateAppHTTPSListener'")
}
}
return nil
}
func (d *DeployerProvider) updateSslListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error {
// 更新 SSL 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#updateappssllistener%E6%9B%B4%E6%96%B0ssl%E7%9B%91%E5%90%AC%E5%99%A8
updateAppSSLListenerReq := &bceappblb.UpdateAppSSLListenerArgs{
ClientToken: generateClientToken(),
ListenerPort: uint16(cloudHttpsListenerPort),
CertIds: []string{cloudCertId},
}
err := d.sdkClient.UpdateAppSSLListener(cloudLoadbalancerId, updateAppSSLListenerReq)
d.logger.Debug("sdk request 'appblb.UpdateAppSSLListener'", slog.Any("request", updateAppSSLListenerReq))
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'appblb.UpdateAppSSLListener'")
}
return nil
}
func createSdkClient(accessKeyId, secretAccessKey, region string) (*bceappblb.Client, error) {
endpoint := ""
if region != "" {
endpoint = fmt.Sprintf("blb.%s.baidubce.com", region)
}
client, err := bceappblb.NewClient(accessKeyId, secretAccessKey, endpoint)
if err != nil {
return nil, err
}
return client, nil
}
func generateClientToken() string {
return strings.ReplaceAll(uuid.New().String(), "-", "")
}

View File

@ -0,0 +1,86 @@
package baiducloudappblb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-appblb"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fRegion string
fLoadbalancerId string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./baiducloud_appblb_test.go -args \
--CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_REGION="bj" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_LOADBALANCERID="your-blb-loadbalancer-id" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_DOMAIN="your-blb-sni-domain"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
ResourceType: provider.RESOURCE_TYPE_LOADBALANCER,
Region: fRegion,
LoadbalancerId: fLoadbalancerId,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,10 @@
package baiducloudappblb
type ResourceType string
const (
// 资源类型:部署到指定负载均衡器。
RESOURCE_TYPE_LOADBALANCER = ResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
RESOURCE_TYPE_LISTENER = ResourceType("listener")
)

View File

@ -0,0 +1,332 @@
package baiducloudblb
import (
"context"
"errors"
"fmt"
"log/slog"
"strconv"
"strings"
bceblb "github.com/baidubce/bce-sdk-go/services/blb"
"github.com/google/uuid"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cert"
"github.com/usual2970/certimate/internal/pkg/utils/sliceutil"
)
type DeployerConfig struct {
// 百度智能云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 百度智能云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 百度智能云区域。
Region string `json:"region"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 负载均衡实例 ID。
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听端口。
// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。
ListenerPort int32 `json:"listenerPort,omitempty"`
// SNI 域名(支持泛域名)。
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。
Domain string `json:"domain,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *bceblb.Client
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 CAS
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_LOADBALANCER:
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
return nil, err
}
case RESOURCE_TYPE_LISTENER:
if err := d.deployToListener(ctx, upres.CertId); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
}
return &deployer.DeployResult{}, nil
}
func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
// 查询 BLB 实例详情
// REF: https://cloud.baidu.com/doc/BLB/s/njwvxnv79#describeloadbalancerdetail%E6%9F%A5%E8%AF%A2blb%E5%AE%9E%E4%BE%8B%E8%AF%A6%E6%83%85
describeLoadBalancerDetailResp, err := d.sdkClient.DescribeLoadBalancerDetail(d.config.LoadbalancerId)
d.logger.Debug("sdk request 'blb.DescribeLoadBalancerAttribute'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("response", describeLoadBalancerDetailResp))
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'blb.DescribeLoadBalancerDetail'")
}
// 获取全部 HTTPS/SSL 监听端口
listeners := make([]struct {
Type string
Port int32
}, 0)
for _, listener := range describeLoadBalancerDetailResp.Listener {
if listener.Type == "HTTPS" || listener.Type == "SSL" {
listenerPort, err := strconv.Atoi(listener.Port)
if err != nil {
continue
}
listeners = append(listeners, struct {
Type string
Port int32
}{
Type: listener.Type,
Port: int32(listenerPort),
})
}
}
// 遍历更新监听证书
if len(listeners) == 0 {
d.logger.Info("no blb listeners to deploy")
} else {
d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners))
var errs []error
for _, listener := range listeners {
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
if d.config.ListenerPort == 0 {
return errors.New("config `listenerPort` is required")
}
// 查询监听
// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#describealllisteners%E6%9F%A5%E8%AF%A2%E6%89%80%E6%9C%89%E7%9B%91%E5%90%AC
describeAllListenersRequest := &bceblb.DescribeListenerArgs{
ListenerPort: uint16(d.config.ListenerPort),
}
describeAllListenersResp, err := d.sdkClient.DescribeAllListeners(d.config.LoadbalancerId, describeAllListenersRequest)
d.logger.Debug("sdk request 'blb.DescribeAllListeners'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("request", describeAllListenersRequest), slog.Any("response", describeAllListenersResp))
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'blb.DescribeAllListeners'")
}
// 获取全部 HTTPS/SSL 监听端口
listeners := make([]struct {
Type string
Port int32
}, 0)
for _, listener := range describeAllListenersResp.AllListenerList {
if listener.ListenerType == "HTTPS" || listener.ListenerType == "SSL" {
listeners = append(listeners, struct {
Type string
Port int32
}{
Type: listener.ListenerType,
Port: int32(listener.ListenerPort),
})
}
}
// 遍历更新监听证书
if len(listeners) == 0 {
d.logger.Info("no blb listeners to deploy")
} else {
d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners))
var errs []error
for _, listener := range listeners {
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *DeployerProvider) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerType string, cloudListenerPort int32, cloudCertId string) error {
switch strings.ToUpper(cloudListenerType) {
case "HTTPS":
return d.updateHttpsListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId)
case "SSL":
return d.updateSslListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId)
default:
return fmt.Errorf("unsupported listener type: %s", cloudListenerType)
}
}
func (d *DeployerProvider) updateHttpsListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error {
// 查询 HTTPS 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#describehttpslisteners%E6%9F%A5%E8%AF%A2https%E7%9B%91%E5%90%AC%E5%99%A8
describeHTTPSListenersReq := &bceblb.DescribeListenerArgs{
ListenerPort: uint16(cloudHttpsListenerPort),
MaxKeys: 1,
}
describeHTTPSListenersResp, err := d.sdkClient.DescribeHTTPSListeners(cloudLoadbalancerId, describeHTTPSListenersReq)
d.logger.Debug("sdk request 'blb.DescribeHTTPSListeners'", slog.String("blbId", cloudLoadbalancerId), slog.Any("request", describeHTTPSListenersReq), slog.Any("response", describeHTTPSListenersResp))
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'blb.DescribeHTTPSListeners'")
} else if len(describeHTTPSListenersResp.ListenerList) == 0 {
return fmt.Errorf("listener %s:%d not found", cloudLoadbalancerId, cloudHttpsListenerPort)
}
if d.config.Domain == "" {
// 未指定 SNI只需部署到监听器
// 更新 HTTPS 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8
updateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{
ClientToken: generateClientToken(),
ListenerPort: uint16(cloudHttpsListenerPort),
CertIds: []string{cloudCertId},
}
err := d.sdkClient.UpdateHTTPSListener(cloudLoadbalancerId, updateHTTPSListenerReq)
d.logger.Debug("sdk request 'blb.UpdateHTTPSListener'", slog.Any("request", updateHTTPSListenerReq))
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'blb.UpdateHTTPSListener'")
}
} else {
// 指定 SNI需部署到扩展域名
// 更新 HTTPS 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8
updateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{
ClientToken: generateClientToken(),
ListenerPort: uint16(cloudHttpsListenerPort),
AdditionalCertDomains: sliceutil.Map(describeHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceblb.AdditionalCertDomainsModel) bceblb.AdditionalCertDomainsModel {
if domain.Host == d.config.Domain {
return bceblb.AdditionalCertDomainsModel{
Host: domain.Host,
CertId: cloudCertId,
}
}
return bceblb.AdditionalCertDomainsModel{
Host: domain.Host,
CertId: domain.CertId,
}
}),
}
err := d.sdkClient.UpdateHTTPSListener(cloudLoadbalancerId, updateHTTPSListenerReq)
d.logger.Debug("sdk request 'blb.UpdateHTTPSListener'", slog.Any("request", updateHTTPSListenerReq))
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'blb.UpdateHTTPSListener'")
}
}
return nil
}
func (d *DeployerProvider) updateSslListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error {
// 更新 SSL 监听器
// REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatessllistener%E6%9B%B4%E6%96%B0ssl%E7%9B%91%E5%90%AC%E5%99%A8
updateSSLListenerReq := &bceblb.UpdateSSLListenerArgs{
ClientToken: generateClientToken(),
ListenerPort: uint16(cloudHttpsListenerPort),
CertIds: []string{cloudCertId},
}
err := d.sdkClient.UpdateSSLListener(cloudLoadbalancerId, updateSSLListenerReq)
d.logger.Debug("sdk request 'blb.UpdateSSLListener'", slog.Any("request", updateSSLListenerReq))
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'blb.UpdateSSLListener'")
}
return nil
}
func createSdkClient(accessKeyId, secretAccessKey, region string) (*bceblb.Client, error) {
endpoint := ""
if region != "" {
endpoint = fmt.Sprintf("blb.%s.baidubce.com", region)
}
client, err := bceblb.NewClient(accessKeyId, secretAccessKey, endpoint)
if err != nil {
return nil, err
}
return client, nil
}
func generateClientToken() string {
return strings.ReplaceAll(uuid.New().String(), "-", "")
}

View File

@ -0,0 +1,86 @@
package baiducloudblb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-blb"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fRegion string
fLoadbalancerId string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./baiducloud_blb_test.go -args \
--CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_REGION="bj" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_LOADBALANCERID="your-blb-loadbalancer-id" \
--CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_DOMAIN="your-blb-sni-domain"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
ResourceType: provider.RESOURCE_TYPE_LOADBALANCER,
Region: fRegion,
LoadbalancerId: fLoadbalancerId,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -0,0 +1,10 @@
package baiducloudblb
type ResourceType string
const (
// 资源类型:部署到指定负载均衡器。
RESOURCE_TYPE_LOADBALANCER = ResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
RESOURCE_TYPE_LISTENER = ResourceType("listener")
)

View File

@ -0,0 +1,68 @@
package baiducloudcert
import (
"context"
"log/slog"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cert"
)
type DeployerConfig struct {
// 百度智能云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 百度智能云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sslUploader: uploader,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 CAS
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
return &deployer.DeployResult{}, nil
}

View File

@ -150,38 +150,40 @@ func createSdkClient(accessKeyId, secretAccessKey, region string) (*hccdn.CdnCli
return client, nil
}
func assign(reqContent *hccdnmodel.UpdateDomainMultiCertificatesRequestBodyContent, target *hccdnmodel.ConfigsGetBody) *hccdnmodel.UpdateDomainMultiCertificatesRequestBodyContent {
func assign(source *hccdnmodel.UpdateDomainMultiCertificatesRequestBodyContent, target *hccdnmodel.ConfigsGetBody) *hccdnmodel.UpdateDomainMultiCertificatesRequestBodyContent {
// `UpdateDomainMultiCertificates` 中不传的字段表示使用默认值、而非保留原值,
// 因此这里需要把原配置中的参数重新赋值回去。
if target == nil {
return reqContent
return source
}
// 华为云 API 中不传的字段表示使用默认值、而非保留原值,因此这里需要把原配置中的参数重新赋值回去。
// 而且蛋疼的是查询接口返回的数据结构和更新接口传入的参数结构不一致,需要做很多转化。
if *target.OriginProtocol == "follow" {
reqContent.AccessOriginWay = hwsdk.Int32Ptr(1)
source.AccessOriginWay = hwsdk.Int32Ptr(1)
} else if *target.OriginProtocol == "http" {
reqContent.AccessOriginWay = hwsdk.Int32Ptr(2)
source.AccessOriginWay = hwsdk.Int32Ptr(2)
} else if *target.OriginProtocol == "https" {
reqContent.AccessOriginWay = hwsdk.Int32Ptr(3)
source.AccessOriginWay = hwsdk.Int32Ptr(3)
}
if target.ForceRedirect != nil {
reqContent.ForceRedirectConfig = &hccdnmodel.ForceRedirect{}
if source.ForceRedirectConfig == nil {
source.ForceRedirectConfig = &hccdnmodel.ForceRedirect{}
}
if target.ForceRedirect.Status == "on" {
reqContent.ForceRedirectConfig.Switch = 1
reqContent.ForceRedirectConfig.RedirectType = target.ForceRedirect.Type
source.ForceRedirectConfig.Switch = 1
source.ForceRedirectConfig.RedirectType = target.ForceRedirect.Type
} else {
reqContent.ForceRedirectConfig.Switch = 0
source.ForceRedirectConfig.Switch = 0
}
}
if target.Https != nil {
if *target.Https.Http2Status == "on" {
reqContent.Http2 = hwsdk.Int32Ptr(1)
source.Http2 = hwsdk.Int32Ptr(1)
}
}
return reqContent
return source
}

View File

@ -0,0 +1,69 @@
package huaweicloudscm
import (
"context"
"log/slog"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-scm"
)
type DeployerConfig struct {
// 华为云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 华为云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sslUploader: uploader,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
d.sslUploader.WithLogger(logger)
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 SCM
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
return &deployer.DeployResult{}, nil
}

View File

@ -79,15 +79,14 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe
// 绑定证书对应的播放域名
// REF: https://cloud.tencent.com/document/product/267/78655
modifyLiveDomainCertBindingsReq := &tclive.ModifyLiveDomainCertBindingsRequest{
DomainInfos: []*tclive.LiveCertDomainInfo{
{
DomainName: common.StringPtr(d.config.Domain),
Status: common.Int64Ptr(1),
},
modifyLiveDomainCertBindingsReq := tclive.NewModifyLiveDomainCertBindingsRequest()
modifyLiveDomainCertBindingsReq.DomainInfos = []*tclive.LiveCertDomainInfo{
{
DomainName: common.StringPtr(d.config.Domain),
Status: common.Int64Ptr(1),
},
CloudCertId: common.StringPtr(upres.CertId),
}
modifyLiveDomainCertBindingsReq.CloudCertId = common.StringPtr(upres.CertId)
modifyLiveDomainCertBindingsResp, err := d.sdkClient.ModifyLiveDomainCertBindings(modifyLiveDomainCertBindingsReq)
d.logger.Debug("sdk request 'live.ModifyLiveDomainCertBindings'", slog.Any("request", modifyLiveDomainCertBindingsReq), slog.Any("response", modifyLiveDomainCertBindingsResp))
if err != nil {

View File

@ -0,0 +1,72 @@
package volcenginecertcenter
import (
"context"
"log/slog"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-certcenter"
)
type DeployerConfig struct {
// 火山引擎 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 火山引擎 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 火山引擎地域。
Region string `json:"region"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: config.AccessKeyId,
AccessKeySecret: config.AccessKeySecret,
Region: config.Region,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sslUploader: uploader,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
d.sslUploader.WithLogger(logger)
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到证书中心
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
return &deployer.DeployResult{}, nil
}

View File

@ -0,0 +1,139 @@
package baiducloudcert
import (
"context"
"fmt"
"log/slog"
"strings"
"time"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/certutil"
bdsdk "github.com/usual2970/certimate/internal/pkg/vendors/baiducloud-sdk/cert"
)
type UploaderConfig struct {
// 百度智能云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 百度智能云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
}
type UploaderProvider struct {
config *UploaderConfig
logger *slog.Logger
sdkClient *bdsdk.Client
}
var _ uploader.Uploader = (*UploaderProvider)(nil)
func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &UploaderProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
if logger == nil {
u.logger = slog.Default()
} else {
u.logger = logger
}
return u
}
func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 遍历证书列表,避免重复上传
// REF: https://cloud.baidu.com/doc/Reference/s/Gjwvz27xu#35-%E6%9F%A5%E7%9C%8B%E8%AF%81%E4%B9%A6%E5%88%97%E8%A1%A8%E8%AF%A6%E6%83%85
listCertDetail, err := u.sdkClient.ListCertDetail()
u.logger.Debug("sdk request 'cert.ListCertDetail'", slog.Any("response", listCertDetail))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cert.ListCertDetail'")
} else {
for _, certDetail := range listCertDetail.Certs {
// 先对比证书通用名称
if !strings.EqualFold(certX509.Subject.CommonName, certDetail.CertCommonName) {
continue
}
// 再对比证书有效期
oldCertNotBefore, _ := time.Parse("2006-01-02T15:04:05Z", certDetail.CertStartTime)
oldCertNotAfter, _ := time.Parse("2006-01-02T15:04:05Z", certDetail.CertStopTime)
if !certX509.NotBefore.Equal(oldCertNotBefore) || !certX509.NotAfter.Equal(oldCertNotAfter) {
continue
}
// 再对比证书多域名
if certDetail.CertDNSNames != strings.Join(certX509.DNSNames, ",") {
continue
}
// 最后对比证书内容
getCertDetailResp, err := u.sdkClient.GetCertRawData(certDetail.CertId)
u.logger.Debug("sdk request 'cert.GetCertRawData'", slog.Any("certId", certDetail.CertId), slog.Any("response", getCertDetailResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cert.GetCertRawData'")
} else {
oldCertX509, err := certutil.ParseCertificateFromPEM(getCertDetailResp.CertServerData)
if err != nil {
continue
}
if !certutil.EqualCertificate(certX509, oldCertX509) {
continue
}
}
// 如果以上信息都一致,则视为已存在相同证书,直接返回
u.logger.Info("ssl certificate already exists")
return &uploader.UploadResult{
CertId: certDetail.CertId,
CertName: certDetail.CertName,
}, nil
}
}
// 创建证书
// REF: https://cloud.baidu.com/doc/Reference/s/Gjwvz27xu#31-%E5%88%9B%E5%BB%BA%E8%AF%81%E4%B9%A6
createCertReq := &bdsdk.CreateCertArgs{}
createCertReq.CertName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
createCertReq.CertServerData = certPem
createCertReq.CertPrivateData = privkeyPem
createCertResp, err := u.sdkClient.CreateCert(createCertReq)
u.logger.Debug("sdk request 'cert.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cert.CreateCert'")
}
return &uploader.UploadResult{
CertId: createCertResp.CertId,
CertName: createCertResp.CertName,
}, nil
}
func createSdkClient(accessKeyId, secretAccessKey string) (*bdsdk.Client, error) {
client, err := bdsdk.NewClient(accessKeyId, secretAccessKey, "")
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -0,0 +1,72 @@
package baiducloudcert_test
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cert"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
)
func init() {
argsPrefix := "CERTIMATE_UPLOADER_BAIDUCLOUDCAS_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./baiducloud_cas_test.go -args \
--CERTIMATE_UPLOADER_BAIDUCLOUDCAS_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_UPLOADER_BAIDUCLOUDCAS_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_UPLOADER_BAIDUCLOUDCAS_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_UPLOADER_BAIDUCLOUDCAS_SECRETACCESSKEY="your-access-key-secret"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
}, "\n"))
uploader, err := provider.NewUploader(&provider.UploaderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
sres, _ := json.Marshal(res)
t.Logf("ok: %s", string(sres))
})
}

View File

@ -27,6 +27,6 @@ type Uploader interface {
// 表示证书上传结果的数据结构,包含上传后的证书 ID、名称和其他数据。
type UploadResult struct {
CertId string `json:"certId"`
CertName string `json:"certName"`
CertName string `json:"certName,omitzero"`
ExtendedData map[string]any `json:"extendedData,omitempty"`
}

View File

@ -0,0 +1,93 @@
package cert
import (
"errors"
"fmt"
"github.com/baidubce/bce-sdk-go/bce"
"github.com/baidubce/bce-sdk-go/http"
"github.com/baidubce/bce-sdk-go/services/cert"
)
func (c *Client) CreateCert(args *CreateCertArgs) (*CreateCertResult, error) {
if args == nil {
return nil, errors.New("unset args")
}
result, err := c.Client.CreateCert(&args.CreateCertArgs)
if err != nil {
return nil, err
}
return &CreateCertResult{CreateCertResult: *result}, nil
}
func (c *Client) ListCerts() (*ListCertResult, error) {
result, err := c.Client.ListCerts()
if err != nil {
return nil, err
}
return &ListCertResult{ListCertResult: *result}, nil
}
func (c *Client) ListCertDetail() (*ListCertDetailResult, error) {
result, err := c.Client.ListCertDetail()
if err != nil {
return nil, err
}
return &ListCertDetailResult{ListCertDetailResult: *result}, nil
}
func (c *Client) GetCertMeta(id string) (*CertificateMeta, error) {
result, err := c.Client.GetCertMeta(id)
if err != nil {
return nil, err
}
return &CertificateMeta{CertificateMeta: *result}, nil
}
func (c *Client) GetCertDetail(id string) (*CertificateDetailMeta, error) {
result, err := c.Client.GetCertDetail(id)
if err != nil {
return nil, err
}
return &CertificateDetailMeta{CertificateDetailMeta: *result}, nil
}
func (c *Client) GetCertRawData(id string) (*CertificateRawData, error) {
result := &CertificateRawData{}
err := bce.NewRequestBuilder(c).
WithMethod(http.GET).
WithURL(cert.URI_PREFIX + cert.REQUEST_CERT_URL + "/" + id + "/rawData").
WithResult(result).
Do()
return result, err
}
func (c *Client) UpdateCertName(id string, args *UpdateCertNameArgs) error {
if args == nil {
return errors.New("unset args")
}
err := c.Client.UpdateCertName(id, &args.UpdateCertNameArgs)
return err
}
func (c *Client) UpdateCertData(id string, args *UpdateCertDataArgs) error {
if args == nil {
return fmt.Errorf("unset args")
}
err := c.Client.UpdateCertData(id, &args.UpdateCertDataArgs)
return err
}
func (c *Client) DeleteCert(id string) error {
err := c.Client.DeleteCert(id)
return err
}

View File

@ -0,0 +1,17 @@
package cert
import (
"github.com/baidubce/bce-sdk-go/services/cert"
)
type Client struct {
*cert.Client
}
func NewClient(ak, sk, endPoint string) (*Client, error) {
client, err := cert.NewClient(ak, sk, endPoint)
if err != nil {
return nil, err
}
return &Client{client}, nil
}

View File

@ -0,0 +1,48 @@
package cert
import "github.com/baidubce/bce-sdk-go/services/cert"
type CreateCertArgs struct {
cert.CreateCertArgs
}
type CreateCertResult struct {
cert.CreateCertResult
}
type UpdateCertNameArgs struct {
cert.UpdateCertNameArgs
}
type CertificateMeta struct {
cert.CertificateMeta
}
type CertificateDetailMeta struct {
cert.CertificateDetailMeta
}
type CertificateRawData struct {
CertId string `json:"certId"`
CertName string `json:"certName"`
CertServerData string `json:"certServerData"`
CertPrivateData string `json:"certPrivateKey"`
CertLinkData string `json:"certLinkData,omitempty"`
CertType int `json:"certType,omitempty"`
}
type ListCertResult struct {
cert.ListCertResult
}
type ListCertDetailResult struct {
cert.ListCertDetailResult
}
type UpdateCertDataArgs struct {
cert.UpdateCertDataArgs
}
type CertInServiceMeta struct {
cert.CertInServiceMeta
}

View File

@ -39,6 +39,11 @@ func (r *AccessRepository) castRecordToModel(record *core.Record) (*domain.Acces
return nil, fmt.Errorf("record is nil")
}
config := make(map[string]any)
if err := record.UnmarshalJSONField("config", &config); err != nil {
return nil, err
}
access := &domain.Access{
Meta: domain.Meta{
Id: record.Id,
@ -47,7 +52,7 @@ func (r *AccessRepository) castRecordToModel(record *core.Record) (*domain.Acces
},
Name: record.GetString("name"),
Provider: record.GetString("provider"),
Config: record.GetString("config"),
Config: config,
}
return access, nil
}

View File

@ -45,7 +45,7 @@ func (n *applyNode) Process(ctx context.Context) error {
n.logger.Info(fmt.Sprintf("skip this application, because %s", skipReason))
return nil
} else if skipReason != "" {
n.logger.Info(fmt.Sprintf("continue to apply, because %s", skipReason))
n.logger.Info(fmt.Sprintf("re-apply, because %s", skipReason))
}
// 初始化申请器

View File

@ -58,7 +58,7 @@ func (n *deployNode) Process(ctx context.Context) error {
n.logger.Info(fmt.Sprintf("skip this deployment, because %s", skipReason))
return nil
} else if skipReason != "" {
n.logger.Info(fmt.Sprintf("continue to deploy, because %s", skipReason))
n.logger.Info(fmt.Sprintf("re-deploy, because %s", skipReason))
}
}

View File

@ -43,7 +43,7 @@ func (n *uploadNode) Process(ctx context.Context) error {
n.logger.Info(fmt.Sprintf("skip this upload, because %s", skipReason))
return nil
} else if skipReason != "" {
n.logger.Info(fmt.Sprintf("continue to upload, because %s", skipReason))
n.logger.Info(fmt.Sprintf("re-upload, because %s", skipReason))
}
// 生成证书实体

View File

@ -21,9 +21,9 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => {
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: AccessModel }>>([]);
useEffect(() => {
const items = filter != null ? accesses.filter(filter) : accesses;
const filteredItems = filter != null ? accesses.filter(filter) : accesses;
setOptions(
items.map((item) => ({
filteredItems.map((item) => ({
key: item.id,
value: item.id,
label: item.name,

View File

@ -1,22 +1,32 @@
import { memo } from "react";
import { memo, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Avatar, Select, type SelectProps, Space, Tag, Typography } from "antd";
import { ACCESS_USAGES, accessProvidersMap } from "@/domain/provider";
import { ACCESS_USAGES, type AccessProvider, accessProvidersMap } from "@/domain/provider";
export type AccessProviderSelectProps = Omit<
SelectProps,
"filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"
>;
> & {
filter?: (record: AccessProvider) => boolean;
};
const AccessProviderSelect = (props: AccessProviderSelectProps) => {
const AccessProviderSelect = ({ filter, ...props }: AccessProviderSelectProps) => {
const { t } = useTranslation();
const options = Array.from(accessProvidersMap.values()).map((item) => ({
key: item.type,
value: item.type,
label: t(item.name),
}));
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: AccessProvider }>>([]);
useEffect(() => {
const allItems = Array.from(accessProvidersMap.values());
const filteredItems = filter != null ? allItems.filter(filter) : allItems;
setOptions(
filteredItems.map((item) => ({
key: item.type,
value: item.type,
label: t(item.name),
data: item,
}))
);
}, [filter]);
const renderOption = (key: string) => {
const provider = accessProvidersMap.get(key);

View File

@ -1,22 +1,32 @@
import { memo } from "react";
import { memo, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Avatar, Select, type SelectProps, Space, Typography } from "antd";
import { applyDNSProvidersMap } from "@/domain/provider";
import { type ApplyDNSProvider, applyDNSProvidersMap } from "@/domain/provider";
export type ApplyDNSProviderSelectProps = Omit<
SelectProps,
"filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"
>;
> & {
filter?: (record: ApplyDNSProvider) => boolean;
};
const ApplyDNSProviderSelect = (props: ApplyDNSProviderSelectProps) => {
const ApplyDNSProviderSelect = ({ filter, ...props }: ApplyDNSProviderSelectProps) => {
const { t } = useTranslation();
const options = Array.from(applyDNSProvidersMap.values()).map((item) => ({
key: item.type,
value: item.type,
label: t(item.name),
}));
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: ApplyDNSProvider }>>([]);
useEffect(() => {
const allItems = Array.from(applyDNSProvidersMap.values());
const filteredItems = filter != null ? allItems.filter(filter) : allItems;
setOptions(
filteredItems.map((item) => ({
key: item.type,
value: item.type,
label: t(item.name),
data: item,
}))
);
}, [filter]);
const renderOption = (key: string) => {
const provider = applyDNSProvidersMap.get(key);

View File

@ -1,22 +1,32 @@
import { memo } from "react";
import { memo, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Avatar, Select, type SelectProps, Space, Typography } from "antd";
import { deployProvidersMap } from "@/domain/provider";
import { type DeployProvider, deployProvidersMap } from "@/domain/provider";
export type DeployProviderSelectProps = Omit<
SelectProps,
"filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"
>;
> & {
filter?: (record: DeployProvider) => boolean;
};
const DeployProviderSelect = (props: DeployProviderSelectProps) => {
const DeployProviderSelect = ({ filter, ...props }: DeployProviderSelectProps) => {
const { t } = useTranslation();
const options = Array.from(deployProvidersMap.values()).map((item) => ({
key: item.type,
value: item.type,
label: t(item.name),
}));
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: DeployProvider }>>([]);
useEffect(() => {
const allItems = Array.from(deployProvidersMap.values());
const filteredItems = filter != null ? allItems.filter(filter) : allItems;
setOptions(
filteredItems.map((item) => ({
key: item.type,
value: item.type,
label: t(item.name),
data: item,
}))
);
}, [filter]);
const renderOption = (key: string) => {
const provider = deployProvidersMap.get(key);

View File

@ -37,6 +37,7 @@ import Show from "@/components/Show";
import { type CertificateModel } from "@/domain/certificate";
import type { WorkflowLogModel } from "@/domain/workflowLog";
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
import { useBrowserTheme } from "@/hooks";
import { listByWorkflowRunId as listCertificatesByWorkflowRunId } from "@/repository/certificate";
import { listByWorkflowRunId as listLogsByWorkflowRunId } from "@/repository/workflowLog";
import { mergeCls } from "@/utils/css";
@ -67,6 +68,7 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
const { t } = useTranslation();
const { token: themeToken } = theme.useToken();
const { theme: browserTheme } = useBrowserTheme();
type Log = Pick<WorkflowLogModel, "timestamp" | "level" | "message" | "data">;
type LogGroup = { id: string; name: string; records: Log[] };
@ -212,7 +214,7 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
}}
trigger={["click"]}
>
<Button icon={<SettingOutlinedIcon />} ghost />
<Button color="primary" icon={<SettingOutlinedIcon />} ghost={browserTheme === "light"} />
</Dropdown>
</div>
</div>

View File

@ -36,6 +36,7 @@ import { validDomainName, validIPv4Address, validIPv6Address } from "@/utils/val
import ApplyNodeConfigFormAWSRoute53Config from "./ApplyNodeConfigFormAWSRoute53Config";
import ApplyNodeConfigFormHuaweiCloudDNSConfig from "./ApplyNodeConfigFormHuaweiCloudDNSConfig";
import ApplyNodeConfigFormJDCloudDNSConfig from "./ApplyNodeConfigFormJDCloudDNSConfig";
import ApplyNodeConfigFormTencentCloudEOConfig from "./ApplyNodeConfigFormTencentCloudEOConfig";
type ApplyNodeConfigFormFieldValues = Partial<WorkflowNodeConfigForApply>;
@ -125,6 +126,19 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
const fieldDomains = Form.useWatch<string>("domains", formInst);
const fieldNameservers = Form.useWatch<string>("nameservers", formInst);
const [showProvider, setShowProvider] = useState(false);
useEffect(() => {
// 通常情况下每个授权信息只对应一个 DNS 提供商,此时无需显示 DNS 提供商字段;
// 如果对应多个(如 AWS 的 Route53、Lightsail腾讯云的 DNS、EdgeOne 等),则显示。
if (fieldProviderAccessId) {
const access = accesses.find((e) => e.id === fieldProviderAccessId);
const providers = Array.from(applyDNSProvidersMap.values()).filter((e) => e.provider === access?.provider);
setShowProvider(providers.length > 1);
} else {
setShowProvider(false);
}
}, [accesses, fieldProviderAccessId]);
const [nestedFormInst] = Form.useForm();
const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeApplyConfigFormProviderConfigForm" });
const nestedFormEl = useMemo(() => {
@ -149,12 +163,15 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
case APPLY_DNS_PROVIDERS.JDCLOUD:
case APPLY_DNS_PROVIDERS.JDCLOUD_DNS:
return <ApplyNodeConfigFormJDCloudDNSConfig {...nestedFormProps} />;
case APPLY_DNS_PROVIDERS.TENCENTCLOUD_EO:
return <ApplyNodeConfigFormTencentCloudEOConfig {...nestedFormProps} />;
}
}, [disabled, initialValues?.providerConfig, fieldProvider, nestedFormInst, nestedFormName]);
const handleProviderSelect = (value: string) => {
if (fieldProvider === value) return;
// 切换 DNS 提供商时联动授权信息
if (initialValues?.provider === value) {
formInst.setFieldValue("providerAccessId", initialValues?.providerAccessId);
onValuesChange?.(formInst.getFieldsValue(true));
@ -169,10 +186,13 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
const handleProviderAccessSelect = (value: string) => {
if (fieldProviderAccessId === value) return;
// DNS 提供商和授权提供商目前一一对应,因此切换授权时,自动切换到相应提供商
// 切换授权信息时联动 DNS 提供商
const access = accesses.find((access) => access.id === value);
formInst.setFieldValue("provider", Array.from(applyDNSProvidersMap.values()).find((provider) => provider.provider === access?.provider)?.type);
onValuesChange?.(formInst.getFieldsValue(true));
const provider = Array.from(applyDNSProvidersMap.values()).find((provider) => provider.provider === access?.provider);
if (fieldProvider !== provider?.type) {
formInst.setFieldValue("provider", provider?.type);
onValuesChange?.(formInst.getFieldsValue(true));
}
};
const handleFormProviderChange = (name: string) => {
@ -252,10 +272,16 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
/>
</Form.Item>
<Form.Item name="provider" label={t("workflow_node.apply.form.provider.label")} hidden rules={[formRule]}>
<Form.Item name="provider" label={t("workflow_node.apply.form.provider.label")} hidden={!showProvider} rules={[formRule]}>
<ApplyDNSProviderSelect
allowClear
disabled
disabled={!showProvider}
filter={(record) => {
if (fieldProviderAccessId) {
return accesses.find((e) => e.id === fieldProviderAccessId)?.provider === record.provider;
}
return true;
}}
placeholder={t("workflow_node.apply.form.provider.placeholder")}
showSearch
onSelect={handleProviderSelect}
@ -294,11 +320,11 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
</label>
<Form.Item name="providerAccessId" rules={[formRule]}>
<AccessSelect
placeholder={t("workflow_node.apply.form.provider_access.placeholder")}
filter={(record) => {
const provider = accessProvidersMap.get(record.provider);
return !!provider?.usages?.includes(ACCESS_USAGES.APPLY);
}}
placeholder={t("workflow_node.apply.form.provider_access.placeholder")}
onChange={handleProviderAccessSelect}
/>
</Form.Item>

View File

@ -0,0 +1,64 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
type ApplyNodeConfigFormTencentCloudEOConfigFieldValues = Nullish<{
zoneId: string;
}>;
export type ApplyNodeConfigFormTencentCloudEOConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: ApplyNodeConfigFormTencentCloudEOConfigFieldValues;
onValuesChange?: (values: ApplyNodeConfigFormTencentCloudEOConfigFieldValues) => void;
};
const initFormModel = (): ApplyNodeConfigFormTencentCloudEOConfigFieldValues => {
return {};
};
const ApplyNodeConfigFormTencentCloudEOConfig = ({
form: formInst,
formName,
disabled,
initialValues,
onValuesChange,
}: ApplyNodeConfigFormTencentCloudEOConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
zoneId: z
.string({ message: t("workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder") })
.nonempty(t("workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder"))
.trim(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="zoneId"
label={t("workflow_node.apply.form.tencentcloud_eo_zone_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.tencentcloud_eo_zone_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder")} />
</Form.Item>
</Form>
);
};
export default ApplyNodeConfigFormTencentCloudEOConfig;

View File

@ -33,6 +33,8 @@ import DeployNodeConfigFormAliyunWAFConfig from "./DeployNodeConfigFormAliyunWAF
import DeployNodeConfigFormAWSACMConfig from "./DeployNodeConfigFormAWSACMConfig";
import DeployNodeConfigFormAWSCloudFrontConfig from "./DeployNodeConfigFormAWSCloudFrontConfig";
import DeployNodeConfigFormAzureKeyVaultConfig from "./DeployNodeConfigFormAzureKeyVaultConfig";
import DeployNodeConfigFormBaiduCloudAppBLBConfig from "./DeployNodeConfigFormBaiduCloudAppBLBConfig";
import DeployNodeConfigFormBaiduCloudBLBConfig from "./DeployNodeConfigFormBaiduCloudBLBConfig";
import DeployNodeConfigFormBaiduCloudCDNConfig from "./DeployNodeConfigFormBaiduCloudCDNConfig";
import DeployNodeConfigFormBaishanCDNConfig from "./DeployNodeConfigFormBaishanCDNConfig";
import DeployNodeConfigFormBaotaPanelConsoleConfig from "./DeployNodeConfigFormBaotaPanelConsoleConfig";
@ -72,6 +74,7 @@ import DeployNodeConfigFormUpyunCDNConfig from "./DeployNodeConfigFormUpyunCDNCo
import DeployNodeConfigFormUpyunFileConfig from "./DeployNodeConfigFormUpyunFileConfig.tsx";
import DeployNodeConfigFormVolcEngineALBConfig from "./DeployNodeConfigFormVolcEngineALBConfig.tsx";
import DeployNodeConfigFormVolcEngineCDNConfig from "./DeployNodeConfigFormVolcEngineCDNConfig.tsx";
import DeployNodeConfigFormVolcEngineCertCenterConfig from "./DeployNodeConfigFormVolcEngineCertCenterConfig.tsx";
import DeployNodeConfigFormVolcEngineCLBConfig from "./DeployNodeConfigFormVolcEngineCLBConfig.tsx";
import DeployNodeConfigFormVolcEngineDCDNConfig from "./DeployNodeConfigFormVolcEngineDCDNConfig.tsx";
import DeployNodeConfigFormVolcEngineImageXConfig from "./DeployNodeConfigFormVolcEngineImageXConfig.tsx";
@ -185,6 +188,10 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
return <DeployNodeConfigFormAWSCloudFrontConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.AZURE_KEYVAULT:
return <DeployNodeConfigFormAzureKeyVaultConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.BAIDUCLOUD_APPBLB:
return <DeployNodeConfigFormBaiduCloudAppBLBConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.BAIDUCLOUD_BLB:
return <DeployNodeConfigFormBaiduCloudBLBConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.BAIDUCLOUD_CDN:
return <DeployNodeConfigFormBaiduCloudCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.BAISHAN_CDN:
@ -263,6 +270,8 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
return <DeployNodeConfigFormVolcEngineALBConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.VOLCENGINE_CDN:
return <DeployNodeConfigFormVolcEngineCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.VOLCENGINE_CERTCENTER:
return <DeployNodeConfigFormVolcEngineCertCenterConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.VOLCENGINE_CLB:
return <DeployNodeConfigFormVolcEngineCLBConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.VOLCENGINE_DCDN:
@ -391,7 +400,6 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
</label>
<Form.Item name="providerAccessId" rules={[formRule]}>
<AccessSelect
placeholder={t("workflow_node.deploy.form.provider_access.placeholder")}
filter={(record) => {
if (fieldProvider) {
return deployProvidersMap.get(fieldProvider)?.provider === record.provider;
@ -400,6 +408,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
const provider = accessProvidersMap.get(record.provider);
return !!provider?.usages?.includes(ACCESS_USAGES.DEPLOY);
}}
placeholder={t("workflow_node.deploy.form.provider_access.placeholder")}
/>
</Form.Item>
</Form.Item>

View File

@ -54,20 +54,8 @@ const DeployNodeConfigFormAliyunCLBConfig = ({
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim(),
listenerPort: z
.union([
z
.number()
.refine(
(v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v),
t("workflow_node.deploy.form.aliyun_clb_listener_port.placeholder")
),
z
.string()
.refine(
(v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v),
t("workflow_node.deploy.form.aliyun_clb_listener_port.placeholder")
),
])
.union([z.number(), z.string()])
.refine((v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v), t("workflow_node.deploy.form.aliyun_clb_listener_port.placeholder"))
.nullish(),
domain: z
.string()

View File

@ -0,0 +1,142 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { validDomainName, validPortNumber } from "@/utils/validators";
type DeployNodeConfigFormBaiduCloudAppBLBConfigFieldValues = Nullish<{
resourceType: string;
region: string;
loadbalancerId?: string;
listenerPort?: string | number;
domain?: string;
}>;
export type DeployNodeConfigFormBaiduCloudAppBLBConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: DeployNodeConfigFormBaiduCloudAppBLBConfigFieldValues;
onValuesChange?: (values: DeployNodeConfigFormBaiduCloudAppBLBConfigFieldValues) => void;
};
const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const;
const RESOURCE_TYPE_LISTENER = "listener" as const;
const initFormModel = (): DeployNodeConfigFormBaiduCloudAppBLBConfigFieldValues => {
return {
listenerPort: 443,
};
};
const DeployNodeConfigFormBaiduCloudAppBLBConfig = ({
form: formInst,
formName,
disabled,
initialValues,
onValuesChange,
}: DeployNodeConfigFormBaiduCloudAppBLBConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
resourceType: z.union([z.literal(RESOURCE_TYPE_LOADBALANCER), z.literal(RESOURCE_TYPE_LISTENER)], {
message: t("workflow_node.deploy.form.baiducloud_appblb_resource_type.placeholder"),
}),
region: z
.string({ message: t("workflow_node.deploy.form.baiducloud_appblb_region.placeholder") })
.nonempty(t("workflow_node.deploy.form.baiducloud_appblb_region.placeholder"))
.trim(),
loadbalancerId: z
.string({ message: t("workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder") })
.min(1, t("workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim(),
listenerPort: z
.union([z.number(), z.string()])
.refine(
(v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v),
t("workflow_node.deploy.form.baiducloud_appblb_listener_port.placeholder")
)
.nullish(),
domain: z
.string()
.nullish()
.refine((v) => {
if (![RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].includes(fieldResourceType)) return true;
return !v || validDomainName(v!, { allowWildcard: true });
}, t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
const fieldResourceType = Form.useWatch("resourceType", formInst);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item name="resourceType" label={t("workflow_node.deploy.form.baiducloud_appblb_resource_type.label")} rules={[formRule]}>
<Select placeholder={t("workflow_node.deploy.form.baiducloud_appblb_resource_type.placeholder")}>
<Select.Option key={RESOURCE_TYPE_LOADBALANCER} value={RESOURCE_TYPE_LOADBALANCER}>
{t("workflow_node.deploy.form.baiducloud_appblb_resource_type.option.loadbalancer.label")}
</Select.Option>
<Select.Option key={RESOURCE_TYPE_LISTENER} value={RESOURCE_TYPE_LISTENER}>
{t("workflow_node.deploy.form.baiducloud_appblb_resource_type.option.listener.label")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="region"
label={t("workflow_node.deploy.form.baiducloud_appblb_region.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.baiducloud_appblb_region.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.baiducloud_appblb_region.placeholder")} />
</Form.Item>
<Form.Item
name="loadbalancerId"
label={t("workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder")} />
</Form.Item>
<Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>
<Form.Item
name="listenerPort"
label={t("workflow_node.deploy.form.baiducloud_appblb_listener_port.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.baiducloud_appblb_listener_port.tooltip") }}></span>}
>
<Input type="number" min={1} max={65535} placeholder={t("workflow_node.deploy.form.baiducloud_appblb_listener_port.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER || fieldResourceType === RESOURCE_TYPE_LISTENER}>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.baiducloud_appblb_snidomain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.baiducloud_appblb_snidomain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.baiducloud_appblb_snidomain.placeholder")} />
</Form.Item>
</Show>
</Form>
);
};
export default DeployNodeConfigFormBaiduCloudAppBLBConfig;

View File

@ -0,0 +1,142 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { validDomainName, validPortNumber } from "@/utils/validators";
type DeployNodeConfigFormBaiduCloudBLBConfigFieldValues = Nullish<{
resourceType: string;
region: string;
loadbalancerId?: string;
listenerPort?: string | number;
domain?: string;
}>;
export type DeployNodeConfigFormBaiduCloudBLBConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: DeployNodeConfigFormBaiduCloudBLBConfigFieldValues;
onValuesChange?: (values: DeployNodeConfigFormBaiduCloudBLBConfigFieldValues) => void;
};
const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const;
const RESOURCE_TYPE_LISTENER = "listener" as const;
const initFormModel = (): DeployNodeConfigFormBaiduCloudBLBConfigFieldValues => {
return {
listenerPort: 443,
};
};
const DeployNodeConfigFormBaiduCloudBLBConfig = ({
form: formInst,
formName,
disabled,
initialValues,
onValuesChange,
}: DeployNodeConfigFormBaiduCloudBLBConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
resourceType: z.union([z.literal(RESOURCE_TYPE_LOADBALANCER), z.literal(RESOURCE_TYPE_LISTENER)], {
message: t("workflow_node.deploy.form.baiducloud_blb_resource_type.placeholder"),
}),
region: z
.string({ message: t("workflow_node.deploy.form.baiducloud_blb_region.placeholder") })
.nonempty(t("workflow_node.deploy.form.baiducloud_blb_region.placeholder"))
.trim(),
loadbalancerId: z
.string({ message: t("workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder") })
.min(1, t("workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim(),
listenerPort: z
.union([z.number(), z.string()])
.refine(
(v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v),
t("workflow_node.deploy.form.baiducloud_blb_listener_port.placeholder")
)
.nullish(),
domain: z
.string()
.nullish()
.refine((v) => {
if (![RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].includes(fieldResourceType)) return true;
return !v || validDomainName(v!, { allowWildcard: true });
}, t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
const fieldResourceType = Form.useWatch("resourceType", formInst);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item name="resourceType" label={t("workflow_node.deploy.form.baiducloud_blb_resource_type.label")} rules={[formRule]}>
<Select placeholder={t("workflow_node.deploy.form.baiducloud_blb_resource_type.placeholder")}>
<Select.Option key={RESOURCE_TYPE_LOADBALANCER} value={RESOURCE_TYPE_LOADBALANCER}>
{t("workflow_node.deploy.form.baiducloud_blb_resource_type.option.loadbalancer.label")}
</Select.Option>
<Select.Option key={RESOURCE_TYPE_LISTENER} value={RESOURCE_TYPE_LISTENER}>
{t("workflow_node.deploy.form.baiducloud_blb_resource_type.option.listener.label")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="region"
label={t("workflow_node.deploy.form.baiducloud_blb_region.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.baiducloud_blb_region.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.baiducloud_blb_region.placeholder")} />
</Form.Item>
<Form.Item
name="loadbalancerId"
label={t("workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder")} />
</Form.Item>
<Show when={fieldResourceType === RESOURCE_TYPE_LISTENER}>
<Form.Item
name="listenerPort"
label={t("workflow_node.deploy.form.baiducloud_blb_listener_port.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.baiducloud_blb_listener_port.tooltip") }}></span>}
>
<Input type="number" min={1} max={65535} placeholder={t("workflow_node.deploy.form.baiducloud_blb_listener_port.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldResourceType === RESOURCE_TYPE_LOADBALANCER || fieldResourceType === RESOURCE_TYPE_LISTENER}>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.baiducloud_blb_snidomain.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.baiducloud_blb_snidomain.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.baiducloud_blb_snidomain.placeholder")} />
</Form.Item>
</Show>
</Form>
);
};
export default DeployNodeConfigFormBaiduCloudBLBConfig;

View File

@ -0,0 +1,59 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
type DeployNodeConfigFormVolcEngineCertCenterConfigFieldValues = Nullish<{
region: string;
}>;
export type DeployNodeConfigFormVolcEngineCertCenterConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: DeployNodeConfigFormVolcEngineCertCenterConfigFieldValues;
onValuesChange?: (values: DeployNodeConfigFormVolcEngineCertCenterConfigFieldValues) => void;
};
const initFormModel = (): DeployNodeConfigFormVolcEngineCertCenterConfigFieldValues => {
return {};
};
const DeployNodeConfigFormVolcEngineCertCenterConfig = ({
form: formInst,
formName,
disabled,
initialValues,
onValuesChange,
}: DeployNodeConfigFormVolcEngineCertCenterConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
region: z
.string({ message: t("workflow_node.deploy.form.volcengine_certcenter_region.placeholder") })
.nonempty(t("workflow_node.deploy.form.volcengine_certcenter_region.placeholder"))
.trim(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item name="region" label={t("workflow_node.deploy.form.volcengine_certcenter_region.label")} rules={[formRule]}>
<Input placeholder={t("workflow_node.deploy.form.volcengine_certcenter_region.placeholder")} />
</Form.Item>
</Form>
);
};
export default DeployNodeConfigFormVolcEngineCertCenterConfig;

View File

@ -165,6 +165,7 @@ export const APPLY_DNS_PROVIDERS = Object.freeze({
RAINYUN: `${ACCESS_PROVIDERS.RAINYUN}`,
TENCENTCLOUD: `${ACCESS_PROVIDERS.TENCENTCLOUD}`, // 兼容旧值,等同于 `TENCENTCLOUD_DNS`
TENCENTCLOUD_DNS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-dns`,
TENCENTCLOUD_EO: `${ACCESS_PROVIDERS.TENCENTCLOUD}-eo`,
VERCEL: `${ACCESS_PROVIDERS.VERCEL}`,
VOLCENGINE: `${ACCESS_PROVIDERS.VOLCENGINE}`, // 兼容旧值,等同于 `VOLCENGINE_DNS`
VOLCENGINE_DNS: `${ACCESS_PROVIDERS.VOLCENGINE}-dns`,
@ -188,6 +189,7 @@ export const applyDNSProvidersMap: Map<ApplyDNSProvider["type"] | string, ApplyD
[
[APPLY_DNS_PROVIDERS.ALIYUN_DNS, "provider.aliyun.dns"],
[APPLY_DNS_PROVIDERS.TENCENTCLOUD_DNS, "provider.tencentcloud.dns"],
[APPLY_DNS_PROVIDERS.TENCENTCLOUD_EO, "provider.tencentcloud.eo"],
[APPLY_DNS_PROVIDERS.BAIDUCLOUD_DNS, "provider.baiducloud.dns"],
[APPLY_DNS_PROVIDERS.HUAWEICLOUD_DNS, "provider.huaweicloud.dns"],
[APPLY_DNS_PROVIDERS.VOLCENGINE_DNS, "provider.volcengine.dns"],
@ -208,7 +210,7 @@ export const applyDNSProvidersMap: Map<ApplyDNSProvider["type"] | string, ApplyD
[APPLY_DNS_PROVIDERS.NS1, "provider.ns1"],
[APPLY_DNS_PROVIDERS.PORKBUN, "provider.porkbun"],
[APPLY_DNS_PROVIDERS.VERCEL, "provider.vercel"],
[APPLY_DNS_PROVIDERS.CMCCCLOUD, "provider.cmcc"],
[APPLY_DNS_PROVIDERS.CMCCCLOUD, "provider.cmcccloud"],
[APPLY_DNS_PROVIDERS.RAINYUN, "provider.rainyun"],
[APPLY_DNS_PROVIDERS.WESTCN, "provider.westcn"],
[APPLY_DNS_PROVIDERS.POWERDNS, "provider.powerdns"],
@ -249,7 +251,10 @@ export const DEPLOY_PROVIDERS = Object.freeze({
AWS_ACM: `${ACCESS_PROVIDERS.AWS}-acm`,
AWS_CLOUDFRONT: `${ACCESS_PROVIDERS.AWS}-cloudfront`,
AZURE_KEYVAULT: `${ACCESS_PROVIDERS.AZURE}-keyvault`,
BAIDUCLOUD_APPBLB: `${ACCESS_PROVIDERS.BAIDUCLOUD}-appblb`,
BAIDUCLOUD_BLB: `${ACCESS_PROVIDERS.BAIDUCLOUD}-blb`,
BAIDUCLOUD_CDN: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cdn`,
BAIDUCLOUD_CERT: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cert`,
BAISHAN_CDN: `${ACCESS_PROVIDERS.BAISHAN}-cdn`,
BAOTAPANEL_CONSOLE: `${ACCESS_PROVIDERS.BAOTAPANEL}-console`,
BAOTAPANEL_SITE: `${ACCESS_PROVIDERS.BAOTAPANEL}-site`,
@ -261,6 +266,7 @@ export const DEPLOY_PROVIDERS = Object.freeze({
GCORE_CDN: `${ACCESS_PROVIDERS.GCORE}-cdn`,
HUAWEICLOUD_CDN: `${ACCESS_PROVIDERS.HUAWEICLOUD}-cdn`,
HUAWEICLOUD_ELB: `${ACCESS_PROVIDERS.HUAWEICLOUD}-elb`,
HUAWEICLOUD_SCM: `${ACCESS_PROVIDERS.HUAWEICLOUD}-scm`,
HUAWEICLOUD_WAF: `${ACCESS_PROVIDERS.HUAWEICLOUD}-waf`,
JDCLOUD_ALB: `${ACCESS_PROVIDERS.JDCLOUD}-alb`,
JDCLOUD_CDN: `${ACCESS_PROVIDERS.JDCLOUD}-cdn`,
@ -290,6 +296,7 @@ export const DEPLOY_PROVIDERS = Object.freeze({
UPYUN_FILE: `${ACCESS_PROVIDERS.UPYUN}-file`,
VOLCENGINE_ALB: `${ACCESS_PROVIDERS.VOLCENGINE}-alb`,
VOLCENGINE_CDN: `${ACCESS_PROVIDERS.VOLCENGINE}-cdn`,
VOLCENGINE_CERTCENTER: `${ACCESS_PROVIDERS.VOLCENGINE}-certcenter`,
VOLCENGINE_CLB: `${ACCESS_PROVIDERS.VOLCENGINE}-clb`,
VOLCENGINE_DCDN: `${ACCESS_PROVIDERS.VOLCENGINE}-dcdn`,
VOLCENGINE_IMAGEX: `${ACCESS_PROVIDERS.VOLCENGINE}-imagex`,
@ -356,10 +363,14 @@ export const deployProvidersMap: Map<DeployProvider["type"] | string, DeployProv
[DEPLOY_PROVIDERS.TENCENTCLOUD_SCF, "provider.tencentcloud.scf", DEPLOY_CATEGORIES.SERVERLESS],
[DEPLOY_PROVIDERS.TENCENTCLOUD_SSL, "provider.tencentcloud.ssl", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.TENCENTCLOUD_SSL_DEPLOY, "provider.tencentcloud.ssl_deploy", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.BAIDUCLOUD_CDN, "provider.baiducloud.cdn", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.BAIDUCLOUD_BLB, "provider.baiducloud.blb", DEPLOY_CATEGORIES.LOADBALANCE],
[DEPLOY_PROVIDERS.BAIDUCLOUD_APPBLB, "provider.baiducloud.appblb", DEPLOY_CATEGORIES.LOADBALANCE],
[DEPLOY_PROVIDERS.BAIDUCLOUD_CERT, "provider.baiducloud.cert", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.HUAWEICLOUD_CDN, "provider.huaweicloud.cdn", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.HUAWEICLOUD_ELB, "provider.huaweicloud.elb", DEPLOY_CATEGORIES.LOADBALANCE],
[DEPLOY_PROVIDERS.HUAWEICLOUD_WAF, "provider.huaweicloud.waf", DEPLOY_CATEGORIES.FIREWALL],
[DEPLOY_PROVIDERS.BAIDUCLOUD_CDN, "provider.baiducloud.cdn", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.HUAWEICLOUD_SCM, "provider.huaweicloud.scm", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.VOLCENGINE_TOS, "provider.volcengine.tos", DEPLOY_CATEGORIES.STORAGE],
[DEPLOY_PROVIDERS.VOLCENGINE_CDN, "provider.volcengine.cdn", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.VOLCENGINE_DCDN, "provider.volcengine.dcdn", DEPLOY_CATEGORIES.CDN],
@ -367,6 +378,7 @@ export const deployProvidersMap: Map<DeployProvider["type"] | string, DeployProv
[DEPLOY_PROVIDERS.VOLCENGINE_ALB, "provider.volcengine.alb", DEPLOY_CATEGORIES.LOADBALANCE],
[DEPLOY_PROVIDERS.VOLCENGINE_IMAGEX, "provider.volcengine.imagex", DEPLOY_CATEGORIES.STORAGE],
[DEPLOY_PROVIDERS.VOLCENGINE_LIVE, "provider.volcengine.live", DEPLOY_CATEGORIES.AV],
[DEPLOY_PROVIDERS.VOLCENGINE_CERTCENTER, "provider.volcengine.certcenter", DEPLOY_CATEGORIES.OTHER],
[DEPLOY_PROVIDERS.JDCLOUD_ALB, "provider.jdcloud.alb", DEPLOY_CATEGORIES.LOADBALANCE],
[DEPLOY_PROVIDERS.JDCLOUD_CDN, "provider.jdcloud.cdn", DEPLOY_CATEGORIES.CDN],
[DEPLOY_PROVIDERS.JDCLOUD_LIVE, "provider.jdcloud.live", DEPLOY_CATEGORIES.AV],

View File

@ -5,8 +5,8 @@
"provider.acmehttpreq": "Http Request (ACME Proxy)",
"provider.aliyun": "Alibaba Cloud",
"provider.aliyun.alb": "Alibaba Cloud - ALB (Application Load Balancer)",
"provider.aliyun.cas": "Alibaba Cloud - CAS (Certificate Management Service)",
"provider.aliyun.cas_deploy": "Alibaba Cloud - via CAS (Certificate Management Service) Deployment Job",
"provider.aliyun.cas": "Alibaba Cloud - Upload to CAS (Certificate Management Service)",
"provider.aliyun.cas_deploy": "Alibaba Cloud - Deploy via CAS (Certificate Management Service)",
"provider.aliyun.cdn": "Alibaba Cloud - CDN (Content Delivery Network)",
"provider.aliyun.clb": "Alibaba Cloud - CLB (Classic Load Balancer)",
"provider.aliyun.dcdn": "Alibaba Cloud - DCDN (Dynamic Route for Content Delivery Network)",
@ -28,7 +28,10 @@
"provider.azure.dns": "Azure - DNS",
"provider.azure.keyvault": "Azure - KeyVault",
"provider.baiducloud": "Baidu Cloud",
"provider.baiducloud.appblb": "Baidu Cloud - AppBLB (Application Baidu Load Balancer)",
"provider.baiducloud.blb": "Baidu Cloud - BLB (Baidu Load Balancer)",
"provider.baiducloud.cdn": "Baidu Cloud - CDN (Content Delivery Network)",
"provider.baiducloud.cert": "Baidu Cloud - Upload to SSL Certificate Service",
"provider.baiducloud.dns": "Baidu Cloud - DNS (Domain Name Service)",
"provider.baishan": "Baishan",
"provider.baishan.cdn": "Baishan - CDN (Content Delivery Network)",
@ -62,6 +65,7 @@
"provider.huaweicloud.cdn": "Huawei Cloud - CDN (Content Delivery Network)",
"provider.huaweicloud.dns": "Huawei Cloud - DNS (Domain Name Service)",
"provider.huaweicloud.elb": "Huawei Cloud - ELB (Elastic Load Balance)",
"provider.huaweicloud.scm": "Huawei Cloud - Upload to SCM (SSL Certificate Manager)",
"provider.huaweicloud.waf": "Huawei Cloud - WAF (Web Application Firewall)",
"provider.jdcloud": "JD Cloud",
"provider.jdcloud.alb": "JD Cloud - ALB (Application Load Balancer)",
@ -94,8 +98,8 @@
"provider.tencentcloud.ecdn": "Tencent Cloud - ECDN (Enterprise Content Delivery Network)",
"provider.tencentcloud.eo": "Tencent Cloud - EdgeOne",
"provider.tencentcloud.scf": "Tencent Cloud - SCF (Serverless Cloud Function)",
"provider.tencentcloud.ssl": "Tencent Cloud - SSL Certificate Service",
"provider.tencentcloud.ssl_deploy": "Tencent Cloud - via SSL Certificate Service Deployment Job",
"provider.tencentcloud.ssl": "Tencent Cloud - Upload to SSL Certificate Service",
"provider.tencentcloud.ssl_deploy": "Tencent Cloud - Deploy via SSL Certificate Service",
"provider.tencentcloud.vod": "Tencent Cloud - VOD (Video on Demand)",
"provider.tencentcloud.waf": "Tencent Cloud - WAF (Web Application Firewall)",
"provider.ucloud": "UCloud",
@ -108,6 +112,7 @@
"provider.volcengine": "Volcengine",
"provider.volcengine.alb": "Volcengine - ALB (Application Load Balancer)",
"provider.volcengine.cdn": "Volcengine - CDN (Content Delivery Network)",
"provider.volcengine.certcenter": "Volcengine - Upload to Certificate Center",
"provider.volcengine.clb": "Volcengine - CLB (Cloud Load Balancer)",
"provider.volcengine.dcdn": "Volcengine - DCDN (Dynamic Content Delivery Network)",
"provider.volcengine.dns": "Volcengine - DNS (Domain Name Service)",

View File

@ -31,6 +31,8 @@
"workflow_node.apply.form.contact_email.label": "Contact email",
"workflow_node.apply.form.contact_email.placeholder": "Please enter contact email",
"workflow_node.apply.form.contact_email.tooltip": "Contact information required for SSL certificate application. Please pay attention to the <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">rate limits</a>.",
"workflow_node.apply.form.challenge_type.label": "Challenge type",
"workflow_node.apply.form.challenge_type.placeholder": "Please select challenge type",
"workflow_node.apply.form.provider.label": "DNS provider",
"workflow_node.apply.form.provider.placeholder": "Please select DNS provider of the domains",
"workflow_node.apply.form.provider_access.label": "DNS provider authorization",
@ -49,6 +51,9 @@
"workflow_node.apply.form.jdcloud_dns_region_id.label": "JD Cloud DNS region ID",
"workflow_node.apply.form.jdcloud_dns_region_id.placeholder": "Please enter JD Cloud DNS region ID (e.g. cn-north-1)",
"workflow_node.apply.form.jdcloud_dns_region_id.tooltip": "For more information, see <a href=\"https://docs.jdcloud.com/en/common-declaration/api/introduction\" target=\"_blank\">https://docs.jdcloud.com/en/common-declaration/api/introduction</a>",
"workflow_node.apply.form.tencentcloud_eo_zone_id.label": "Tencent Cloud EdgeOne zone ID",
"workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder": "Please enter Tencent Cloud EdgeOne zone ID",
"workflow_node.apply.form.tencentcloud_eo_zone_id.tooltip": "For more information, see <a href=\"https://console.tencentcloud.com/edgeone\" target=\"_blank\">https://console.tencentcloud.com/edgeone</a>",
"workflow_node.apply.form.advanced_config.label": "Advanced settings",
"workflow_node.apply.form.key_algorithm.label": "Certificate key algorithm",
"workflow_node.apply.form.key_algorithm.placeholder": "Please select certificate key algorithm",
@ -75,7 +80,7 @@
"workflow_node.apply.form.skip_before_expiry_days.prefix": "If the certificate expiration time exceeds",
"workflow_node.apply.form.skip_before_expiry_days.suffix": ", skip to re-apply.",
"workflow_node.apply.form.skip_before_expiry_days.unit": "days",
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "Be careful not to exceed the validity period limit of the CA, otherwise the certificate may never be renewed.",
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "Be careful not to exceed the validity period limit of the issued certificate, otherwise the certificate may never be renewed.",
"workflow_node.deploy.label": "Deployment",
"workflow_node.deploy.search.provider.placeholder": "Search deploy target ...",
@ -222,6 +227,38 @@
"workflow_node.deploy.form.azure_keyvault_name.label": "Azure KeyVault name",
"workflow_node.deploy.form.azure_keyvault_name.placeholder": "Please enter Azure KeyVault name",
"workflow_node.deploy.form.azure_keyvault_name.tooltip": "For more information, see <a href=\"https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates\" target=\"_blank\">https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates</a>",
"workflow_node.deploy.form.baiducloud_appblb_resource_type.label": "Resource type",
"workflow_node.deploy.form.baiducloud_appblb_resource_type.placeholder": "Please select resource type",
"workflow_node.deploy.form.baiducloud_appblb_resource_type.option.loadbalancer.label": "BLB load balancer",
"workflow_node.deploy.form.baiducloud_appblb_resource_type.option.listener.label": "BLB listener",
"workflow_node.deploy.form.baiducloud_appblb_region.label": "Baidu Cloud BLB region",
"workflow_node.deploy.form.baiducloud_appblb_region.placeholder": "Please enter Baidu Cloud BLB region (e.g. bj)",
"workflow_node.deploy.form.baiducloud_appblb_region.tooltip": "For more information, see <a href=\"https://cloud.baidu.com/doc/BLB/s/cjwvxnzix\" target=\"_blank\">https://cloud.baidu.com/doc/BLB/s/cjwvxnzix</a>",
"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.label": "Baidu Cloud BLB load balancer ID",
"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder": "Please enter Baidu Cloud BLB load balancer ID",
"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.tooltip": "For more information, see <a href=\"https://console.bce.baidu.com/blb/#/appblb/list\" target=\"_blank\">https://console.bce.baidu.com/blb/#/appblb/list</a>",
"workflow_node.deploy.form.baiducloud_appblb_listener_port.label": "Baidu Cloud BLB listener port",
"workflow_node.deploy.form.baiducloud_appblb_listener_port.placeholder": "Please enter Baidu Cloud BLB listener port",
"workflow_node.deploy.form.baiducloud_appblb_listener_port.tooltip": "For more information, see <a href=\"https://console.bce.baidu.com/blb/#/appblb/list\" target=\"_blank\">https://console.bce.baidu.com/blb/#/appblb/list</a>",
"workflow_node.deploy.form.baiducloud_appblb_snidomain.label": "Baidu Cloud BLB SNI domain (Optional)",
"workflow_node.deploy.form.baiducloud_appblb_snidomain.placeholder": "Please enter Baidu Cloud BLB SNI domain name",
"workflow_node.deploy.form.baiducloud_appblb_snidomain.tooltip": "For more information, see <a href=\"https://console.bce.baidu.com/blb/#/appblb/list\" target=\"_blank\">https://console.bce.baidu.com/blb/#/appblb/list</a>",
"workflow_node.deploy.form.baiducloud_blb_resource_type.label": "Resource type",
"workflow_node.deploy.form.baiducloud_blb_resource_type.placeholder": "Please select resource type",
"workflow_node.deploy.form.baiducloud_blb_resource_type.option.loadbalancer.label": "BLB load balancer",
"workflow_node.deploy.form.baiducloud_blb_resource_type.option.listener.label": "BLB listener",
"workflow_node.deploy.form.baiducloud_blb_region.label": "Baidu Cloud BLB region",
"workflow_node.deploy.form.baiducloud_blb_region.placeholder": "Please enter Baidu Cloud BLB region (e.g. bj)",
"workflow_node.deploy.form.baiducloud_blb_region.tooltip": "For more information, see <a href=\"https://cloud.baidu.com/doc/BLB/s/cjwvxnzix\" target=\"_blank\">https://cloud.baidu.com/doc/BLB/s/cjwvxnzix</a>",
"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.label": "Baidu Cloud BLB load balancer ID",
"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder": "Please enter Baidu Cloud BLB load balancer ID",
"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.tooltip": "For more information, see <a href=\"https://console.bce.baidu.com/blb/#/blb/list\" target=\"_blank\">https://console.bce.baidu.com/blb/#/blb/list</a>",
"workflow_node.deploy.form.baiducloud_blb_listener_port.label": "Baidu Cloud BLB listener port",
"workflow_node.deploy.form.baiducloud_blb_listener_port.placeholder": "Please enter Baidu Cloud BLB listener port",
"workflow_node.deploy.form.baiducloud_blb_listener_port.tooltip": "For more information, see <a href=\"https://console.bce.baidu.com/blb/#/blb/list\" target=\"_blank\">https://console.bce.baidu.com/blb/#/blb/list</a>",
"workflow_node.deploy.form.baiducloud_blb_snidomain.label": "Baidu Cloud BLB SNI domain (Optional)",
"workflow_node.deploy.form.baiducloud_blb_snidomain.placeholder": "Please enter Baidu Cloud BLB SNI domain name",
"workflow_node.deploy.form.baiducloud_blb_snidomain.tooltip": "For more information, see <a href=\"https://console.bce.baidu.com/blb/#/blb/list\" target=\"_blank\">https://console.bce.baidu.com/blb/#/blb/list</a>",
"workflow_node.deploy.form.baiducloud_cdn_domain.label": "Baidu Cloud CDN domain",
"workflow_node.deploy.form.baiducloud_cdn_domain.placeholder": "Please enter Baidu Cloud CDN domain name",
"workflow_node.deploy.form.baiducloud_cdn_domain.tooltip": "For more information, see <a href=\"https://console.bce.baidu.com/cdn\" target=\"_blank\">https://console.bce.baidu.com/cdn</a>",
@ -541,12 +578,14 @@
"workflow_node.deploy.form.volcengine_alb_listener_id.label": "VolcEngine ALB listener ID",
"workflow_node.deploy.form.volcengine_alb_listener_id.placeholder": "Please enter VolcEngine ALB listener ID",
"workflow_node.deploy.form.volcengine_alb_listener_id.tooltip": "For more information, see <a href=\"https://console.volcengine.com/alb\" target=\"_blank\">https://console.volcengine.com/alb</a>",
"workflow_node.deploy.form.volcengine_clb_snidomain.label": "VolcEngine CLB SNI domain (Optional)",
"workflow_node.deploy.form.volcengine_clb_snidomain.placeholder": "Please enter VolcEngine CLB SNI domain name",
"workflow_node.deploy.form.volcengine_clb_snidomain.tooltip": "For more information, see <a href=\"https://console.volcengine.com/alb\" target=\"_blank\">https://console.volcengine.com/alb</a>",
"workflow_node.deploy.form.volcengine_alb_snidomain.label": "VolcEngine CLB SNI domain (Optional)",
"workflow_node.deploy.form.volcengine_alb_snidomain.placeholder": "Please enter VolcEngine CLB SNI domain name",
"workflow_node.deploy.form.volcengine_alb_snidomain.tooltip": "For more information, see <a href=\"https://console.volcengine.com/alb\" target=\"_blank\">https://console.volcengine.com/alb</a>",
"workflow_node.deploy.form.volcengine_cdn_domain.label": "VolcEngine CDN domain",
"workflow_node.deploy.form.volcengine_cdn_domain.placeholder": "Please enter VolcEngine CDN domain name",
"workflow_node.deploy.form.volcengine_cdn_domain.tooltip": "For more information, see <a href=\"https://console.volcengine.com/cdn/homepage\" target=\"_blank\">https://console.volcengine.com/cdn/homepage</a>",
"workflow_node.deploy.form.volcengine_certcenter_region.label": "VolcEngine Certificate Center region",
"workflow_node.deploy.form.volcengine_certcenter_region.placeholder": "Please enter VolcEngine Certificate Center region (e.g. cn-beijing)",
"workflow_node.deploy.form.volcengine_clb_resource_type.label": "Resource type",
"workflow_node.deploy.form.volcengine_clb_resource_type.placeholder": "Please select resource type",
"workflow_node.deploy.form.volcengine_clb_resource_type.option.loadbalancer.label": "CLB load balancer",
@ -596,15 +635,6 @@
"workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "skip",
"workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "not skip",
"workflow_node.notify.label": "Notification",
"workflow_node.notify.form.subject.label": "Subject",
"workflow_node.notify.form.subject.placeholder": "Please enter subject",
"workflow_node.notify.form.message.label": "Message",
"workflow_node.notify.form.message.placeholder": "Please enter message",
"workflow_node.notify.form.channel.label": "Channel",
"workflow_node.notify.form.channel.placeholder": "Please select channel",
"workflow_node.notify.form.channel.button": "Configure",
"workflow_node.upload.label": "Upload",
"workflow_node.upload.form.domains.label": "Domains",
"workflow_node.upload.form.domains.placholder": "Please select certificate file",
@ -615,6 +645,15 @@
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
"workflow_node.upload.form.private_key.button": "Choose file ...",
"workflow_node.notify.label": "Notification",
"workflow_node.notify.form.subject.label": "Subject",
"workflow_node.notify.form.subject.placeholder": "Please enter subject",
"workflow_node.notify.form.message.label": "Message",
"workflow_node.notify.form.message.placeholder": "Please enter message",
"workflow_node.notify.form.channel.label": "Channel",
"workflow_node.notify.form.channel.placeholder": "Please select channel",
"workflow_node.notify.form.channel.button": "Configure",
"workflow_node.end.label": "End",
"workflow_node.branch.label": "Parallel branch",

View File

@ -5,7 +5,7 @@
"provider.acmehttpreq": "Http Request (ACME Proxy)",
"provider.aliyun": "阿里云",
"provider.aliyun.alb": "阿里云 - 应用型负载均衡 ALB",
"provider.aliyun.cas": "阿里云 - 数字证书管理服务 CAS",
"provider.aliyun.cas": "阿里云 - 上传到数字证书管理服务 CAS",
"provider.aliyun.cas_deploy": "阿里云 - 通过数字证书管理服务 CAS 创建部署任务",
"provider.aliyun.cdn": "阿里云 - 内容分发网络 CDN",
"provider.aliyun.clb": "阿里云 - 传统型负载均衡 CLB",
@ -28,7 +28,10 @@
"provider.azure.dns": "Azure - DNS",
"provider.azure.keyvault": "Azure - KeyVault",
"provider.baiducloud": "百度智能云",
"provider.baiducloud.appblb": "百度智能云 - 应用型负载均衡 BLB",
"provider.baiducloud.blb": "百度智能云 - 普通型负载均衡 BLB",
"provider.baiducloud.cdn": "百度智能云 - 内容分发网络 CDN",
"provider.baiducloud.cert": "百度智能云 - 上传到 SSL 证书服务",
"provider.baiducloud.dns": "百度智能云 - 智能云解析 DNS",
"provider.baishan": "白山云",
"provider.baishan.cdn": "白山云 - 内容分发网络 CDN",
@ -62,6 +65,7 @@
"provider.huaweicloud.cdn": "华为云 - 内容分发网络 CDN",
"provider.huaweicloud.dns": "华为云 - 云解析 DNS",
"provider.huaweicloud.elb": "华为云 - 弹性负载均衡 ELB",
"provider.huaweicloud.scm": "华为云 - 上传到云证书管理服务 SCM",
"provider.huaweicloud.waf": "华为云 - Web 应用防火墙 WAF",
"provider.jdcloud": "京东云",
"provider.jdcloud.alb": "京东云 - 应用负载均衡 ALB",
@ -94,7 +98,7 @@
"provider.tencentcloud.ecdn": "腾讯云 - 全站加速网络 ECDN",
"provider.tencentcloud.eo": "腾讯云 - 边缘安全加速平台 EdgeOne",
"provider.tencentcloud.scf": "腾讯云 - 云函数 SCF",
"provider.tencentcloud.ssl": "腾讯云 - SSL 证书服务",
"provider.tencentcloud.ssl": "腾讯云 - 上传到 SSL 证书服务",
"provider.tencentcloud.ssl_deploy": "腾讯云 - 通过 SSL 证书服务创建部署任务",
"provider.tencentcloud.vod": "腾讯云 - 云点播 VOD",
"provider.tencentcloud.waf": "腾讯云 - Web 应用防火墙 WAF",
@ -108,6 +112,7 @@
"provider.volcengine": "火山引擎",
"provider.volcengine.alb": "火山引擎 - 应用型负载均衡 ALB",
"provider.volcengine.cdn": "火山引擎 - 内容分发网络 CDN",
"provider.volcengine.certcenter": "火山引擎 - 上传到证书中心",
"provider.volcengine.clb": "火山引擎 - 负载均衡 CLB",
"provider.volcengine.dcdn": "火山引擎 - 全站加速 DCDN",
"provider.volcengine.dns": "火山引擎 - 云解析 DNS",

View File

@ -22,7 +22,7 @@
"workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:",
"workflow_node.start.form.trigger_cron.guide": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。也不要总是设置为每日零时,以免遭遇证书颁发机构的流量高峰。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Lets Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Lets Encrypt (ACME) 客户端启动时间应当随机?</a>",
"workflow_node.apply.label": "申请",
"workflow_node.apply.label": "申请证书",
"workflow_node.apply.form.domains.label": "域名",
"workflow_node.apply.form.domains.placeholder": "请输入域名(多个值请用半角分号隔开)",
"workflow_node.apply.form.domains.tooltip": "泛域名表示形式为:*.example.com",
@ -31,6 +31,8 @@
"workflow_node.apply.form.contact_email.label": "联系邮箱",
"workflow_node.apply.form.contact_email.placeholder": "请输入联系邮箱",
"workflow_node.apply.form.contact_email.tooltip": "申请签发 SSL 证书时所需的联系方式。请注意 Let's Encrypt 账户注册的速率限制。<a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">点此了解更多</a>。",
"workflow_node.apply.form.challenge_type.label": "质询方式",
"workflow_node.apply.form.challenge_type.placeholder": "请选择质询方式",
"workflow_node.apply.form.provider.label": "DNS 提供商",
"workflow_node.apply.form.provider.placeholder": "请选择 DNS 提供商",
"workflow_node.apply.form.provider_access.label": "DNS 提供商授权",
@ -49,6 +51,9 @@
"workflow_node.apply.form.jdcloud_dns_region_id.label": "京东云 DNS 服务地域 ID",
"workflow_node.apply.form.jdcloud_dns_region_id.placeholder": "请输入京东云 DNS 服务地域 ID例如cn-north-1",
"workflow_node.apply.form.jdcloud_dns_region_id.tooltip": "这是什么?请参阅 <a href=\"https://docs.jdcloud.com/cn/common-declaration/api/introduction\" target=\"_blank\">https://docs.jdcloud.com/cn/common-declaration/api/introduction</a>",
"workflow_node.apply.form.tencentcloud_eo_zone_id.label": "腾讯云 EdgeOne 站点 ID",
"workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder": "请输入腾讯云 EdgeOne 站点 ID",
"workflow_node.apply.form.tencentcloud_eo_zone_id.tooltip": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/edgeone\" target=\"_blank\">https://console.cloud.tencent.com/edgeone</a>",
"workflow_node.apply.form.advanced_config.label": "高级设置",
"workflow_node.apply.form.key_algorithm.label": "数字证书算法",
"workflow_node.apply.form.key_algorithm.placeholder": "请选择数字证书算法",
@ -75,9 +80,9 @@
"workflow_node.apply.form.skip_before_expiry_days.prefix": "当上次签发的证书距到期时间超过",
"workflow_node.apply.form.skip_before_expiry_days.suffix": "时,跳过重新申请。",
"workflow_node.apply.form.skip_before_expiry_days.unit": "天",
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "注意不要超过 CA 的证书有效期限制,否则证书可能永远不会续期。",
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "注意不要超过颁发的证书最大有效期,否则证书可能永远不会续期。",
"workflow_node.deploy.label": "部署",
"workflow_node.deploy.label": "部署证书",
"workflow_node.deploy.search.provider.placeholder": "搜索部署目标……",
"workflow_node.deploy.form.provider.label": "部署目标",
"workflow_node.deploy.form.provider.placeholder": "请选择部署目标",
@ -222,6 +227,38 @@
"workflow_node.deploy.form.azure_keyvault_name.label": "Azure KeyVault 名称",
"workflow_node.deploy.form.azure_keyvault_name.placeholder": "请输入 Azure KeyVault 名称",
"workflow_node.deploy.form.azure_keyvault_name.tooltip": "这是什么?请参阅 <a href=\"https://learn.microsoft.com/zh-cn/azure/key-vault/general/about-keys-secrets-certificates\" target=\"_blank\">https://learn.microsoft.com/zh-cn/azure/key-vault/general/about-keys-secrets-certificates</a>",
"workflow_node.deploy.form.baiducloud_appblb_resource_type.label": "证书替换方式",
"workflow_node.deploy.form.baiducloud_appblb_resource_type.placeholder": "请选择证书替换方式",
"workflow_node.deploy.form.baiducloud_appblb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS/SSL 监听的证书",
"workflow_node.deploy.form.baiducloud_appblb_resource_type.option.listener.label": "替换指定负载均衡监听的证书",
"workflow_node.deploy.form.baiducloud_appblb_region.label": "百度智能云 BLB 服务地域",
"workflow_node.deploy.form.baiducloud_appblb_region.placeholder": "请输入百度智能云 BLB 服务地域例如bj",
"workflow_node.deploy.form.baiducloud_appblb_region.tooltip": "这是什么?请参阅 <a href=\"https://cloud.baidu.com/doc/BLB/s/cjwvxnzix\" target=\"_blank\">https://cloud.baidu.com/doc/BLB/s/cjwvxnzix</a>",
"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.label": "百度智能云 BLB 负载均衡器 ID",
"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder": "请输入百度智能云 BLB 负载均衡器 ID",
"workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.tooltip": "这是什么?请参阅 <a href=\"https://console.bce.baidu.com/blb/#/appblb/list\" target=\"_blank\">https://console.bce.baidu.com/blb/#/appblb/list</a>",
"workflow_node.deploy.form.baiducloud_appblb_listener_port.label": "百度智能云 BLB 监听端口",
"workflow_node.deploy.form.baiducloud_appblb_listener_port.placeholder": "请输入百度智能云 BLB 监听端口",
"workflow_node.deploy.form.baiducloud_appblb_listener_port.tooltip": "这是什么?请参阅 <a href=\"https://console.bce.baidu.com/blb/#/appblb/list\" target=\"_blank\">https://console.bce.baidu.com/blb/#/appblb/list</a>",
"workflow_node.deploy.form.baiducloud_appblb_snidomain.label": "百度智能云 BLB 扩展域名(可选)",
"workflow_node.deploy.form.baiducloud_appblb_snidomain.placeholder": "请输入百度智能云 BLB 扩展域名(支持泛域名)",
"workflow_node.deploy.form.baiducloud_appblb_snidomain.tooltip": "这是什么?请参阅 <a href=\"https://console.bce.baidu.com/blb/#/appblb/list\" target=\"_blank\">https://console.bce.baidu.com/blb/#/appblb/list</a><br><br>不填写时,将替换监听器的默认证书。",
"workflow_node.deploy.form.baiducloud_blb_resource_type.label": "证书替换方式",
"workflow_node.deploy.form.baiducloud_blb_resource_type.placeholder": "请选择证书替换方式",
"workflow_node.deploy.form.baiducloud_blb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS/SSL 监听的证书",
"workflow_node.deploy.form.baiducloud_blb_resource_type.option.listener.label": "替换指定负载均衡监听的证书",
"workflow_node.deploy.form.baiducloud_blb_region.label": "百度智能云 BLB 服务地域",
"workflow_node.deploy.form.baiducloud_blb_region.placeholder": "请输入百度智能云 BLB 服务地域例如bj",
"workflow_node.deploy.form.baiducloud_blb_region.tooltip": "这是什么?请参阅 <a href=\"https://cloud.baidu.com/doc/BLB/s/cjwvxnzix\" target=\"_blank\">https://cloud.baidu.com/doc/BLB/s/cjwvxnzix</a>",
"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.label": "百度智能云 BLB 负载均衡器 ID",
"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder": "请输入百度智能云 BLB 负载均衡器 ID",
"workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.tooltip": "这是什么?请参阅 <a href=\"https://console.bce.baidu.com/blb/#/blb/list\" target=\"_blank\">https://console.bce.baidu.com/blb/#/blb/list</a>",
"workflow_node.deploy.form.baiducloud_blb_listener_port.label": "百度智能云 BLB 监听端口",
"workflow_node.deploy.form.baiducloud_blb_listener_port.placeholder": "请输入百度智能云 BLB 监听端口",
"workflow_node.deploy.form.baiducloud_blb_listener_port.tooltip": "这是什么?请参阅 <a href=\"https://console.bce.baidu.com/blb/#/blb/list\" target=\"_blank\">https://console.bce.baidu.com/blb/#/blb/list</a>",
"workflow_node.deploy.form.baiducloud_blb_snidomain.label": "百度智能云 BLB 扩展域名(可选)",
"workflow_node.deploy.form.baiducloud_blb_snidomain.placeholder": "请输入百度智能云 BLB 扩展域名(支持泛域名)",
"workflow_node.deploy.form.baiducloud_blb_snidomain.tooltip": "这是什么?请参阅 <a href=\"https://console.bce.baidu.com/blb/#/blb/list\" target=\"_blank\">https://console.bce.baidu.com/blb/#/blb/list</a><br><br>不填写时,将替换监听器的默认证书。",
"workflow_node.deploy.form.baiducloud_cdn_domain.label": "百度智能云 CDN 加速域名",
"workflow_node.deploy.form.baiducloud_cdn_domain.placeholder": "请输入百度智能云 CDN 加速域名(支持泛域名)",
"workflow_node.deploy.form.baiducloud_cdn_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.bce.baidu.com/cdn\" target=\"_blank\">https://console.bce.baidu.com/cdn</a>",
@ -541,12 +578,14 @@
"workflow_node.deploy.form.volcengine_alb_listener_id.label": "火山引擎 ALB 监听器 ID",
"workflow_node.deploy.form.volcengine_alb_listener_id.placeholder": "请输入火山引擎 ALB 监听器 ID",
"workflow_node.deploy.form.volcengine_alb_listener_id.tooltip": "这是什么?请参阅 <a href=\"https://console.volcengine.com/alb\" target=\"_blank\">https://console.volcengine.com/alb</a>",
"workflow_node.deploy.form.volcengine_clb_snidomain.label": "火山引擎 ALB 扩展域名(可选)",
"workflow_node.deploy.form.volcengine_clb_snidomain.placeholder": "请输入火山引擎 ALB 扩展域名(支持泛域名)",
"workflow_node.deploy.form.volcengine_clb_snidomain.tooltip": "这是什么?请参阅 <a href=\"https://console.volcengine.com/alb\" target=\"_blank\">https://console.volcengine.com/alb</a><br><br>不填写时,将替换监听器的默认证书。",
"workflow_node.deploy.form.volcengine_alb_snidomain.label": "火山引擎 ALB 扩展域名(可选)",
"workflow_node.deploy.form.volcengine_alb_snidomain.placeholder": "请输入火山引擎 ALB 扩展域名(支持泛域名)",
"workflow_node.deploy.form.volcengine_alb_snidomain.tooltip": "这是什么?请参阅 <a href=\"https://console.volcengine.com/alb\" target=\"_blank\">https://console.volcengine.com/alb</a><br><br>不填写时,将替换监听器的默认证书。",
"workflow_node.deploy.form.volcengine_cdn_domain.label": "火山引擎 CDN 加速域名",
"workflow_node.deploy.form.volcengine_cdn_domain.placeholder": "请输入火山引擎 CDN 加速域名(支持泛域名)",
"workflow_node.deploy.form.volcengine_cdn_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.volcengine.com/cdn/homepage\" target=\"_blank\">https://console.volcengine.com/cdn/homepage</a>",
"workflow_node.deploy.form.volcengine_certcenter_region.label": "火山引擎证书中心服务地域",
"workflow_node.deploy.form.volcengine_certcenter_region.placeholder": "请输入火山引擎证书中心服务地域例如cn-beijing",
"workflow_node.deploy.form.volcengine_clb_resource_type.label": "证书替换方式",
"workflow_node.deploy.form.volcengine_clb_resource_type.placeholder": "请选择证书替换方式",
"workflow_node.deploy.form.volcengine_clb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS 监听的证书",
@ -596,16 +635,7 @@
"workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "跳过",
"workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "不跳过",
"workflow_node.notify.label": "通知",
"workflow_node.notify.form.subject.label": "通知主题",
"workflow_node.notify.form.subject.placeholder": "请输入通知主题",
"workflow_node.notify.form.message.label": "通知内容",
"workflow_node.notify.form.message.placeholder": "请输入通知内容",
"workflow_node.notify.form.channel.label": "通知渠道",
"workflow_node.notify.form.channel.placeholder": "请选择通知渠道",
"workflow_node.notify.form.channel.button": "去配置",
"workflow_node.upload.label": "上传",
"workflow_node.upload.label": "上传证书",
"workflow_node.upload.form.domains.label": "域名",
"workflow_node.upload.form.domains.placeholder": "上传证书文件后显示",
"workflow_node.upload.form.certificate.label": "证书文件PEM 格式)",
@ -615,6 +645,15 @@
"workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
"workflow_node.upload.form.private_key.button": "选择文件",
"workflow_node.notify.label": "推送通知",
"workflow_node.notify.form.subject.label": "通知主题",
"workflow_node.notify.form.subject.placeholder": "请输入通知主题",
"workflow_node.notify.form.message.label": "通知内容",
"workflow_node.notify.form.message.placeholder": "请输入通知内容",
"workflow_node.notify.form.channel.label": "通知渠道",
"workflow_node.notify.form.channel.placeholder": "请选择通知渠道",
"workflow_node.notify.form.channel.button": "去配置",
"workflow_node.end.label": "结束",
"workflow_node.branch.label": "并行分支",

View File

@ -1,4 +1,5 @@
import { Navigate, Outlet } from "react-router-dom";
import { Layout } from "antd";
import Version from "@/components/Version";
import { getAuthStore } from "@/repository/admin";
@ -10,11 +11,13 @@ const AuthLayout = () => {
}
return (
<div className="container">
<Outlet />
<Layout className="h-screen">
<div className="container">
<Outlet />
<Version className="fixed bottom-4 right-8" />
</div>
<Version className="fixed bottom-4 right-8" />
</div>
</Layout>
);
};

View File

@ -2,7 +2,7 @@ import { ClientResponseError } from "pocketbase";
export const getErrMsg = (error: unknown): string => {
if (error instanceof ClientResponseError) {
return error.response != null ? getErrMsg(error.response) : error.message;
return Object.keys(error.response ?? {}).length ? getErrMsg(error.response) : error.message;
} else if (error instanceof Error) {
return error.message;
} else if (typeof error === "object" && error != null) {