From c9e6bd0c2f178812042097e1487282942e098186 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 19 May 2025 22:51:17 +0800 Subject: [PATCH] feat: new deployment provider: lecdn --- internal/deployer/providers.go | 22 +++ internal/domain/access.go | 9 + internal/domain/provider.go | 4 +- .../1panel-console/1panel_console.go | 1 + .../providers/1panel-site/1panel_site.go | 1 + .../providers/aliyun-clb/aliyun_clb_test.go | 4 +- .../deployer/providers/aliyun-fc/aliyun_fc.go | 1 + .../deployer/providers/flexcdn/flexcdn.go | 1 + .../providers/flexcdn/flexcdn_test.go | 8 +- .../core/deployer/providers/goedge/goedge.go | 1 + .../deployer/providers/goedge/goedge_test.go | 8 +- .../core/deployer/providers/lecdn/consts.go | 8 + .../core/deployer/providers/lecdn/lecdn.go | 176 ++++++++++++++++++ .../deployer/providers/lecdn/lecdn_test.go | 87 +++++++++ .../providers/safeline/safeline_test.go | 4 +- .../core/deployer/providers/ssh/ssh_test.go | 4 +- .../notifier/providers/email/email_test.go | 4 +- internal/pkg/sdk3rd/flexcdn/api.go | 18 +- internal/pkg/sdk3rd/goedge/api.go | 18 +- internal/pkg/sdk3rd/lecdn/v3/client/api.go | 49 +++++ internal/pkg/sdk3rd/lecdn/v3/client/client.go | 98 ++++++++++ internal/pkg/sdk3rd/lecdn/v3/client/models.go | 46 +++++ internal/pkg/sdk3rd/lecdn/v3/master/api.go | 49 +++++ internal/pkg/sdk3rd/lecdn/v3/master/client.go | 98 ++++++++++ internal/pkg/sdk3rd/lecdn/v3/master/models.go | 47 +++++ internal/pkg/sdk3rd/upyun/console/api.go | 36 ++-- internal/pkg/utils/map/getter.go | 110 ++++++----- ui/public/imgs/providers/lecdn.svg | 1 + ui/src/components/access/AccessForm.tsx | 3 + .../access/AccessFormLeCDNConfig.tsx | 85 +++++++++ .../workflow/node/DeployNodeConfigForm.tsx | 3 + .../node/DeployNodeConfigFormLeCDNConfig.tsx | 103 ++++++++++ ui/src/domain/access.ts | 10 + ui/src/domain/provider.ts | 4 + ui/src/i18n/locales/en/nls.access.json | 31 ++- ui/src/i18n/locales/en/nls.provider.json | 1 + .../i18n/locales/en/nls.workflow.nodes.json | 9 + ui/src/i18n/locales/zh/nls.access.json | 31 ++- ui/src/i18n/locales/zh/nls.provider.json | 1 + .../i18n/locales/zh/nls.workflow.nodes.json | 89 +++++---- 40 files changed, 1128 insertions(+), 155 deletions(-) create mode 100644 internal/pkg/core/deployer/providers/lecdn/consts.go create mode 100644 internal/pkg/core/deployer/providers/lecdn/lecdn.go create mode 100644 internal/pkg/core/deployer/providers/lecdn/lecdn_test.go create mode 100644 internal/pkg/sdk3rd/lecdn/v3/client/api.go create mode 100644 internal/pkg/sdk3rd/lecdn/v3/client/client.go create mode 100644 internal/pkg/sdk3rd/lecdn/v3/client/models.go create mode 100644 internal/pkg/sdk3rd/lecdn/v3/master/api.go create mode 100644 internal/pkg/sdk3rd/lecdn/v3/master/client.go create mode 100644 internal/pkg/sdk3rd/lecdn/v3/master/models.go create mode 100644 ui/public/imgs/providers/lecdn.svg create mode 100644 ui/src/components/access/AccessFormLeCDNConfig.tsx create mode 100644 ui/src/components/workflow/node/DeployNodeConfigFormLeCDNConfig.tsx diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 8870fe97..53951300 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -53,6 +53,7 @@ import ( pJDCloudLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/jdcloud-live" pJDCloudVOD "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/jdcloud-vod" pK8sSecret "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/k8s-secret" + pLeCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/lecdn" pLocal "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local" pNetlifySite "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/netlify-site" pProxmoxVE "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/proxmoxve" @@ -729,6 +730,27 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer } } + case domain.DeploymentProviderTypeLeCDN: + { + access := domain.AccessConfigForLeCDN{} + if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + deployer, err := pLeCDN.NewDeployer(&pLeCDN.DeployerConfig{ + ApiUrl: access.ApiUrl, + ApiVersion: access.ApiVersion, + ApiRole: access.ApiRole, + Username: access.Username, + Password: access.Password, + AllowInsecureConnections: access.AllowInsecureConnections, + ResourceType: pLeCDN.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), + CertificateId: maputil.GetInt64(options.ProviderServiceConfig, "certificateId"), + ClientId: maputil.GetInt64(options.ProviderServiceConfig, "clientId"), + }) + return deployer, err + } + case domain.DeploymentProviderTypeLocal: { deployer, err := pLocal.NewDeployer(&pLocal.DeployerConfig{ diff --git a/internal/domain/access.go b/internal/domain/access.go index 30e52412..a2fd8a4a 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -193,6 +193,15 @@ type AccessConfigForLarkBot struct { WebhookUrl string `json:"webhookUrl"` } +type AccessConfigForLeCDN struct { + ApiUrl string `json:"apiUrl"` + ApiVersion string `json:"apiVersion"` + ApiRole string `json:"apiRole"` + Username string `json:"username"` + Password string `json:"password"` + AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` +} + type AccessConfigForMattermost struct { ServerUrl string `json:"serverUrl"` Username string `json:"username"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index fcbaa031..d8091eb8 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -49,7 +49,7 @@ const ( AccessProviderTypeLarkBot = AccessProviderType("larkbot") AccessProviderTypeLetsEncrypt = AccessProviderType("letsencrypt") AccessProviderTypeLetsEncryptStaging = AccessProviderType("letsencryptstaging") - AccessProviderTypeLeCDN = AccessProviderType("lecdn") // LeCDN(预留) + AccessProviderTypeLeCDN = AccessProviderType("lecdn") AccessProviderTypeLocal = AccessProviderType("local") AccessProviderTypeMattermost = AccessProviderType("mattermost") AccessProviderTypeNamecheap = AccessProviderType("namecheap") @@ -208,7 +208,7 @@ const ( DeploymentProviderTypeJDCloudLive = DeploymentProviderType(AccessProviderTypeJDCloud + "-live") DeploymentProviderTypeJDCloudVOD = DeploymentProviderType(AccessProviderTypeJDCloud + "-vod") DeploymentProviderTypeKubernetesSecret = DeploymentProviderType(AccessProviderTypeKubernetes + "-secret") - DeploymentProviderTypeLeCDN = DeploymentProviderType(AccessProviderTypeLeCDN) // LeCDN(预留) + DeploymentProviderTypeLeCDN = DeploymentProviderType(AccessProviderTypeLeCDN) DeploymentProviderTypeLocal = DeploymentProviderType(AccessProviderTypeLocal) DeploymentProviderTypeNetlifySite = DeploymentProviderType(AccessProviderTypeNetlify + "-site") DeploymentProviderTypeProxmoxVE = DeploymentProviderType(AccessProviderTypeProxmoxVE) diff --git a/internal/pkg/core/deployer/providers/1panel-console/1panel_console.go b/internal/pkg/core/deployer/providers/1panel-console/1panel_console.go index 98073585..e81b264f 100644 --- a/internal/pkg/core/deployer/providers/1panel-console/1panel_console.go +++ b/internal/pkg/core/deployer/providers/1panel-console/1panel_console.go @@ -16,6 +16,7 @@ type DeployerConfig struct { // 1Panel 地址。 ApiUrl string `json:"apiUrl"` // 1Panel 版本。 + // 可取值 "v1"、"v2"。 ApiVersion string `json:"apiVersion"` // 1Panel 接口密钥。 ApiKey string `json:"apiKey"` diff --git a/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go b/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go index afecaf74..690e5242 100644 --- a/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go +++ b/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go @@ -19,6 +19,7 @@ type DeployerConfig struct { // 1Panel 地址。 ApiUrl string `json:"apiUrl"` // 1Panel 版本。 + // 可取值 "v1"、"v2"。 ApiVersion string `json:"apiVersion"` // 1Panel 接口密钥。 ApiKey string `json:"apiKey"` diff --git a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb_test.go b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb_test.go index 3b8ce12d..dfa46173 100644 --- a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb_test.go +++ b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb_test.go @@ -18,7 +18,7 @@ var ( fAccessKeySecret string fRegion string fLoadbalancerId string - fListenerPort int + fListenerPort int64 fDomain string ) @@ -31,7 +31,7 @@ func init() { flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "") - flag.IntVar(&fListenerPort, argsPrefix+"LISTENERPORT", 443, "") + flag.Int64Var(&fListenerPort, argsPrefix+"LISTENERPORT", 443, "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } diff --git a/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go b/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go index 8557068c..426aa3a6 100644 --- a/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go +++ b/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go @@ -22,6 +22,7 @@ type DeployerConfig struct { // 阿里云地域。 Region string `json:"region"` // 服务版本。 + // 可取值 "2.0"、"3.0"。 ServiceVersion string `json:"serviceVersion"` // 自定义域名(支持泛域名)。 Domain string `json:"domain"` diff --git a/internal/pkg/core/deployer/providers/flexcdn/flexcdn.go b/internal/pkg/core/deployer/providers/flexcdn/flexcdn.go index 3d0f05e9..a12ed164 100644 --- a/internal/pkg/core/deployer/providers/flexcdn/flexcdn.go +++ b/internal/pkg/core/deployer/providers/flexcdn/flexcdn.go @@ -19,6 +19,7 @@ type DeployerConfig struct { // FlexCDN URL。 ApiUrl string `json:"apiUrl"` // FlexCDN 用户角色。 + // 可取值 "user"、"admin"。 ApiRole string `json:"apiRole"` // FlexCDN AccessKeyId。 AccessKeyId string `json:"accessKeyId"` diff --git a/internal/pkg/core/deployer/providers/flexcdn/flexcdn_test.go b/internal/pkg/core/deployer/providers/flexcdn/flexcdn_test.go index 4a693dc9..b9b8de07 100644 --- a/internal/pkg/core/deployer/providers/flexcdn/flexcdn_test.go +++ b/internal/pkg/core/deployer/providers/flexcdn/flexcdn_test.go @@ -17,7 +17,7 @@ var ( fApiUrl string fAccessKeyId string fAccessKey string - fCertificateId int + fCertificateId int64 ) func init() { @@ -28,7 +28,7 @@ func init() { flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") - flag.IntVar(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "") + flag.Int64Var(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "") } /* @@ -45,7 +45,7 @@ Shell command to run this test: func TestDeploy(t *testing.T) { flag.Parse() - t.Run("Deploy", func(t *testing.T) { + t.Run("Deploy_ToCertificate", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), @@ -63,7 +63,7 @@ func TestDeploy(t *testing.T) { AccessKey: fAccessKey, AllowInsecureConnections: true, ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, - CertificateId: int64(fCertificateId), + CertificateId: fCertificateId, }) if err != nil { t.Errorf("err: %+v", err) diff --git a/internal/pkg/core/deployer/providers/goedge/goedge.go b/internal/pkg/core/deployer/providers/goedge/goedge.go index 73eade64..ecae774e 100644 --- a/internal/pkg/core/deployer/providers/goedge/goedge.go +++ b/internal/pkg/core/deployer/providers/goedge/goedge.go @@ -19,6 +19,7 @@ type DeployerConfig struct { // GoEdge URL。 ApiUrl string `json:"apiUrl"` // GoEdge 用户角色。 + // 可取值 "user"、"admin"。 ApiRole string `json:"apiRole"` // GoEdge AccessKeyId。 AccessKeyId string `json:"accessKeyId"` diff --git a/internal/pkg/core/deployer/providers/goedge/goedge_test.go b/internal/pkg/core/deployer/providers/goedge/goedge_test.go index 928fb420..d10f931c 100644 --- a/internal/pkg/core/deployer/providers/goedge/goedge_test.go +++ b/internal/pkg/core/deployer/providers/goedge/goedge_test.go @@ -17,7 +17,7 @@ var ( fApiUrl string fAccessKeyId string fAccessKey string - fCertificateId int + fCertificateId int64 ) func init() { @@ -28,7 +28,7 @@ func init() { flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") - flag.IntVar(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "") + flag.Int64Var(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "") } /* @@ -45,7 +45,7 @@ Shell command to run this test: func TestDeploy(t *testing.T) { flag.Parse() - t.Run("Deploy", func(t *testing.T) { + t.Run("Deploy_ToCertificate", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), @@ -63,7 +63,7 @@ func TestDeploy(t *testing.T) { AccessKey: fAccessKey, AllowInsecureConnections: true, ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, - CertificateId: int64(fCertificateId), + CertificateId: fCertificateId, }) if err != nil { t.Errorf("err: %+v", err) diff --git a/internal/pkg/core/deployer/providers/lecdn/consts.go b/internal/pkg/core/deployer/providers/lecdn/consts.go new file mode 100644 index 00000000..f5b7c0c9 --- /dev/null +++ b/internal/pkg/core/deployer/providers/lecdn/consts.go @@ -0,0 +1,8 @@ +package lecdn + +type ResourceType string + +const ( + // 资源类型:替换指定证书。 + RESOURCE_TYPE_CERTIFICATE = ResourceType("certificate") +) diff --git a/internal/pkg/core/deployer/providers/lecdn/lecdn.go b/internal/pkg/core/deployer/providers/lecdn/lecdn.go new file mode 100644 index 00000000..1ad88dcf --- /dev/null +++ b/internal/pkg/core/deployer/providers/lecdn/lecdn.go @@ -0,0 +1,176 @@ +package lecdn + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "log/slog" + "net/url" + "time" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + leclientsdkv3 "github.com/usual2970/certimate/internal/pkg/sdk3rd/lecdn/v3/client" + lemastersdkv3 "github.com/usual2970/certimate/internal/pkg/sdk3rd/lecdn/v3/master" +) + +type DeployerConfig struct { + // LeCDN URL。 + ApiUrl string `json:"apiUrl"` + // LeCDN 版本。 + // 可取值 "v3"。 + ApiVersion string `json:"apiVersion"` + // LeCDN 用户角色。 + // 可取值 "client"、"master"。 + ApiRole string `json:"apiRole"` + // LeCDN 用户名。 + Username string `json:"accessKeyId"` + // LeCDN 用户密码。 + Password string `json:"accessKey"` + // 是否允许不安全的连接。 + AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` + // 部署资源类型。 + ResourceType ResourceType `json:"resourceType"` + // 证书 ID。 + // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 + CertificateId int64 `json:"certificateId,omitempty"` + // 客户 ID。 + // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时选填。 + ClientId int64 `json:"clientId,omitempty"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sdkClient interface{} +} + +var _ deployer.Deployer = (*DeployerProvider)(nil) + +const ( + apiVersionV3 = "v3" + + apiRoleClient = "client" + apiRoleMaster = "master" +) + +func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.ApiUrl, config.ApiVersion, config.ApiRole, config.Username, config.Password, config.AllowInsecureConnections) + if err != nil { + return nil, fmt.Errorf("failed to create sdk client: %w", err) + } + + return &DeployerProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + }, 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) { + // 根据部署资源类型决定部署方式 + switch d.config.ResourceType { + case RESOURCE_TYPE_CERTIFICATE: + if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { + return nil, err + } + + default: + return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) + } + + return &deployer.DeployResult{}, nil +} + +func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM string, privkeyPEM string) error { + if d.config.CertificateId == 0 { + return errors.New("config `certificateId` is required") + } + + // 修改证书 + // REF: https://wdk0pwf8ul.feishu.cn/wiki/YE1XwCRIHiLYeKkPupgcXrlgnDd + switch sdkClient := d.sdkClient.(type) { + case *leclientsdkv3.Client: + updateSSLCertReq := &leclientsdkv3.UpdateCertificateRequest{ + Name: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()), + Description: "upload from certimate", + Type: "upload", + SSLPEM: certPEM, + SSLKey: privkeyPEM, + AutoRenewal: false, + } + updateSSLCertResp, err := sdkClient.UpdateCertificate(d.config.CertificateId, updateSSLCertReq) + d.logger.Debug("sdk request 'lecdn.UpdateCertificate'", slog.Int64("certId", d.config.CertificateId), slog.Any("request", updateSSLCertReq), slog.Any("response", updateSSLCertResp)) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'lecdn.UpdateCertificate': %w", err) + } + + case *lemastersdkv3.Client: + updateSSLCertReq := &lemastersdkv3.UpdateCertificateRequest{ + ClientId: d.config.ClientId, + Name: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()), + Description: "upload from certimate", + Type: "upload", + SSLPEM: certPEM, + SSLKey: privkeyPEM, + AutoRenewal: false, + } + updateSSLCertResp, err := sdkClient.UpdateCertificate(d.config.CertificateId, updateSSLCertReq) + d.logger.Debug("sdk request 'lecdn.UpdateCertificate'", slog.Int64("certId", d.config.CertificateId), slog.Any("request", updateSSLCertReq), slog.Any("response", updateSSLCertResp)) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'lecdn.UpdateCertificate': %w", err) + } + + default: + panic("sdk client is not implemented") + } + + return nil +} + +func createSdkClient(apiUrl, apiVersion, apiRole, username, password string, skipTlsVerify bool) (interface{}, error) { + if _, err := url.Parse(apiUrl); err != nil { + return nil, errors.New("invalid lecdn api url") + } + + if username == "" { + return nil, errors.New("invalid lecdn username") + } + + if password == "" { + return nil, errors.New("invalid lecdn password") + } + + if apiVersion == apiVersionV3 && apiRole == apiRoleClient { + // v3 版客户端 + client := leclientsdkv3.NewClient(apiUrl, username, password) + if skipTlsVerify { + client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true}) + } + + return client, nil + } else if apiVersion == apiVersionV3 && apiRole == apiRoleMaster { + // v3 版主控端 + client := lemastersdkv3.NewClient(apiUrl, username, password) + if skipTlsVerify { + client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true}) + } + + return client, nil + } + + return nil, fmt.Errorf("invalid lecdn api version or user role") +} diff --git a/internal/pkg/core/deployer/providers/lecdn/lecdn_test.go b/internal/pkg/core/deployer/providers/lecdn/lecdn_test.go new file mode 100644 index 00000000..cbaa4523 --- /dev/null +++ b/internal/pkg/core/deployer/providers/lecdn/lecdn_test.go @@ -0,0 +1,87 @@ +package lecdn_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/lecdn" +) + +var ( + fInputCertPath string + fInputKeyPath string + fApiUrl string + fApiVersion string + fUsername string + fPassword string + fCertificateId int64 +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_LECDN_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "") + flag.StringVar(&fApiVersion, argsPrefix+"APIVERSION", "v3", "") + flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") + flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "") + flag.Int64Var(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "") +} + +/* +Shell command to run this test: + + go test -v ./lecdn_test.go -args \ + --CERTIMATE_DEPLOYER_LECDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_LECDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_LECDN_APIURL="http://127.0.0.1:5090" \ + --CERTIMATE_DEPLOYER_LECDN_USERNAME="your-username" \ + --CERTIMATE_DEPLOYER_LECDN_PASSWORD="your-password" \ + --CERTIMATE_DEPLOYER_LECDN_CERTIFICATEID="your-cerficiate-id" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy_ToCertificate", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("APIURL: %v", fApiUrl), + fmt.Sprintf("APIVERSION: %v", fApiVersion), + fmt.Sprintf("USERNAME: %v", fUsername), + fmt.Sprintf("PASSWORD: %v", fPassword), + fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), + }, "\n")) + + deployer, err := provider.NewDeployer(&provider.DeployerConfig{ + ApiUrl: fApiUrl, + ApiVersion: fApiVersion, + ApiRole: "user", + Username: fUsername, + Password: fPassword, + AllowInsecureConnections: true, + ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, + CertificateId: fCertificateId, + }) + 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) + }) +} diff --git a/internal/pkg/core/deployer/providers/safeline/safeline_test.go b/internal/pkg/core/deployer/providers/safeline/safeline_test.go index 294086c8..67fe6755 100644 --- a/internal/pkg/core/deployer/providers/safeline/safeline_test.go +++ b/internal/pkg/core/deployer/providers/safeline/safeline_test.go @@ -16,7 +16,7 @@ var ( fInputKeyPath string fApiUrl string fApiToken string - fCertificateId int + fCertificateId int64 ) func init() { @@ -26,7 +26,7 @@ func init() { flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "") flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") - flag.IntVar(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "") + flag.Int64Var(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "") } /* diff --git a/internal/pkg/core/deployer/providers/ssh/ssh_test.go b/internal/pkg/core/deployer/providers/ssh/ssh_test.go index b63471d1..ae908185 100644 --- a/internal/pkg/core/deployer/providers/ssh/ssh_test.go +++ b/internal/pkg/core/deployer/providers/ssh/ssh_test.go @@ -15,7 +15,7 @@ var ( fInputCertPath string fInputKeyPath string fSshHost string - fSshPort int + fSshPort int64 fSshUsername string fSshPassword string fOutputCertPath string @@ -28,7 +28,7 @@ func init() { flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fSshHost, argsPrefix+"SSHHOST", "", "") - flag.IntVar(&fSshPort, argsPrefix+"SSHPORT", 0, "") + flag.Int64Var(&fSshPort, argsPrefix+"SSHPORT", 0, "") flag.StringVar(&fSshUsername, argsPrefix+"SSHUSERNAME", "", "") flag.StringVar(&fSshPassword, argsPrefix+"SSHPASSWORD", "", "") flag.StringVar(&fOutputCertPath, argsPrefix+"OUTPUTCERTPATH", "", "") diff --git a/internal/pkg/core/notifier/providers/email/email_test.go b/internal/pkg/core/notifier/providers/email/email_test.go index 30bfba07..cf0669ca 100644 --- a/internal/pkg/core/notifier/providers/email/email_test.go +++ b/internal/pkg/core/notifier/providers/email/email_test.go @@ -17,7 +17,7 @@ const ( var ( fSmtpHost string - fSmtpPort int + fSmtpPort int64 fSmtpTLS bool fUsername string fPassword string @@ -29,7 +29,7 @@ func init() { argsPrefix := "CERTIMATE_NOTIFIER_EMAIL_" flag.StringVar(&fSmtpHost, argsPrefix+"SMTPHOST", "", "") - flag.IntVar(&fSmtpPort, argsPrefix+"SMTPPORT", 0, "") + flag.Int64Var(&fSmtpPort, argsPrefix+"SMTPPORT", 0, "") flag.BoolVar(&fSmtpTLS, argsPrefix+"SMTPTLS", false, "") flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "") diff --git a/internal/pkg/sdk3rd/flexcdn/api.go b/internal/pkg/sdk3rd/flexcdn/api.go index 07fb2b34..5008fdf4 100644 --- a/internal/pkg/sdk3rd/flexcdn/api.go +++ b/internal/pkg/sdk3rd/flexcdn/api.go @@ -7,7 +7,13 @@ import ( "time" ) -func (c *Client) getAccessToken() error { +func (c *Client) ensureAccessTokenExists() error { + c.accessTokenMtx.Lock() + defer c.accessTokenMtx.Unlock() + if c.accessToken != "" && c.accessTokenExp.After(time.Now()) { + return nil + } + req := &getAPIAccessTokenRequest{ Type: c.apiRole, AccessKeyId: c.accessKeyId, @@ -22,22 +28,18 @@ func (c *Client) getAccessToken() error { if err := json.Unmarshal(res.Body(), &resp); err != nil { return fmt.Errorf("flexcdn api error: failed to unmarshal response: %w", err) } else if resp.GetCode() != 200 { - return fmt.Errorf("flexcdn get access token failed: code: %d, message: %s", resp.GetCode(), resp.GetMessage()) + return fmt.Errorf("flexcdn get access token failed: code='%d', message='%s'", resp.GetCode(), resp.GetMessage()) } - c.accessTokenMtx.Lock() c.accessToken = resp.Data.Token c.accessTokenExp = time.Unix(resp.Data.ExpiresAt, 0) - c.accessTokenMtx.Unlock() return nil } func (c *Client) UpdateSSLCert(req *UpdateSSLCertRequest) (*UpdateSSLCertResponse, error) { - if c.accessToken == "" || c.accessTokenExp.Before(time.Now()) { - if err := c.getAccessToken(); err != nil { - return nil, err - } + if err := c.ensureAccessTokenExists(); err != nil { + return nil, err } resp := &UpdateSSLCertResponse{} diff --git a/internal/pkg/sdk3rd/goedge/api.go b/internal/pkg/sdk3rd/goedge/api.go index f0f60944..4589f70c 100644 --- a/internal/pkg/sdk3rd/goedge/api.go +++ b/internal/pkg/sdk3rd/goedge/api.go @@ -7,7 +7,13 @@ import ( "time" ) -func (c *Client) getAccessToken() error { +func (c *Client) ensureAccessTokenExists() error { + c.accessTokenMtx.Lock() + defer c.accessTokenMtx.Unlock() + if c.accessToken != "" && c.accessTokenExp.After(time.Now()) { + return nil + } + req := &getAPIAccessTokenRequest{ Type: c.apiRole, AccessKeyId: c.accessKeyId, @@ -22,22 +28,18 @@ func (c *Client) getAccessToken() error { if err := json.Unmarshal(res.Body(), &resp); err != nil { return fmt.Errorf("goedge api error: failed to unmarshal response: %w", err) } else if resp.GetCode() != 200 { - return fmt.Errorf("goedge get access token failed: code: %d, message: %s", resp.GetCode(), resp.GetMessage()) + return fmt.Errorf("goedge get access token failed: code='%d', message='%s'", resp.GetCode(), resp.GetMessage()) } - c.accessTokenMtx.Lock() c.accessToken = resp.Data.Token c.accessTokenExp = time.Unix(resp.Data.ExpiresAt, 0) - c.accessTokenMtx.Unlock() return nil } func (c *Client) UpdateSSLCert(req *UpdateSSLCertRequest) (*UpdateSSLCertResponse, error) { - if c.accessToken == "" || c.accessTokenExp.Before(time.Now()) { - if err := c.getAccessToken(); err != nil { - return nil, err - } + if err := c.ensureAccessTokenExists(); err != nil { + return nil, err } resp := &UpdateSSLCertResponse{} diff --git a/internal/pkg/sdk3rd/lecdn/v3/client/api.go b/internal/pkg/sdk3rd/lecdn/v3/client/api.go new file mode 100644 index 00000000..ffdc70e3 --- /dev/null +++ b/internal/pkg/sdk3rd/lecdn/v3/client/api.go @@ -0,0 +1,49 @@ +package client + +import ( + "encoding/json" + "fmt" + "net/http" +) + +func (c *Client) ensureAccessTokenExists() error { + c.accessTokenMtx.Lock() + defer c.accessTokenMtx.Unlock() + if c.accessToken != "" { + return nil + } + + req := &loginRequest{ + Username: c.username, + Password: c.password, + } + res, err := c.sendRequest(http.MethodPost, "/login", req) + if err != nil { + return err + } + + resp := &loginResponse{} + if err := json.Unmarshal(res.Body(), &resp); err != nil { + return fmt.Errorf("lecdn api error: failed to unmarshal response: %w", err) + } else if resp.GetCode() != 200 { + return fmt.Errorf("lecdn get token failed: code='%d', message='%s'", resp.GetCode(), resp.GetMessage()) + } + + c.accessToken = resp.Data.Token + + return nil +} + +func (c *Client) UpdateCertificate(certId int64, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { + if certId == 0 { + return nil, fmt.Errorf("lecdn api error: invalid parameter: CertId") + } + + if err := c.ensureAccessTokenExists(); err != nil { + return nil, err + } + + resp := &UpdateCertificateResponse{} + err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/certificate/%d", certId), req, resp) + return resp, err +} diff --git a/internal/pkg/sdk3rd/lecdn/v3/client/client.go b/internal/pkg/sdk3rd/lecdn/v3/client/client.go new file mode 100644 index 00000000..218499fa --- /dev/null +++ b/internal/pkg/sdk3rd/lecdn/v3/client/client.go @@ -0,0 +1,98 @@ +package client + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "net/http" + "strings" + "sync" + "time" + + "github.com/go-resty/resty/v2" +) + +type Client struct { + apiHost string + username string + password string + + accessToken string + accessTokenMtx sync.Mutex + + client *resty.Client +} + +func NewClient(apiHost, username, password string) *Client { + client := resty.New() + + return &Client{ + apiHost: strings.TrimRight(apiHost, "/"), + username: username, + password: password, + client: client, + } +} + +func (c *Client) WithTimeout(timeout time.Duration) *Client { + c.client.SetTimeout(timeout) + return c +} + +func (c *Client) WithTLSConfig(config *tls.Config) *Client { + c.client.SetTLSClientConfig(config) + return c +} + +func (c *Client) sendRequest(method string, path string, params interface{}) (*resty.Response, error) { + req := c.client.R().SetBasicAuth(c.username, c.password) + if strings.EqualFold(method, http.MethodGet) { + qs := make(map[string]string) + if params != nil { + temp := make(map[string]any) + jsonb, _ := json.Marshal(params) + json.Unmarshal(jsonb, &temp) + for k, v := range temp { + if v != nil { + qs[k] = fmt.Sprintf("%v", v) + } + } + } + + req = req. + SetHeader("Authorization", "Bearer "+c.accessToken). + SetQueryParams(qs) + } else { + req = req. + SetHeader("Content-Type", "application/json"). + SetHeader("Authorization", "Bearer "+c.accessToken). + SetBody(params) + } + + resp, err := req.Execute(method, c.apiHost+"/prod-api"+path) + if err != nil { + return resp, fmt.Errorf("lecdn api error: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("lecdn api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) sendRequestWithResult(method string, path string, params interface{}, result BaseResponse) error { + resp, err := c.sendRequest(method, path, params) + if err != nil { + if resp != nil { + json.Unmarshal(resp.Body(), &result) + } + return err + } + + if err := json.Unmarshal(resp.Body(), &result); err != nil { + return fmt.Errorf("lecdn api error: failed to unmarshal response: %w", err) + } else if errcode := result.GetCode(); errcode != 200 { + return fmt.Errorf("lecdn api error: code='%d', message='%s'", errcode, result.GetMessage()) + } + + return nil +} diff --git a/internal/pkg/sdk3rd/lecdn/v3/client/models.go b/internal/pkg/sdk3rd/lecdn/v3/client/models.go new file mode 100644 index 00000000..a4fecf1c --- /dev/null +++ b/internal/pkg/sdk3rd/lecdn/v3/client/models.go @@ -0,0 +1,46 @@ +package client + +type BaseResponse interface { + GetCode() int32 + GetMessage() string +} + +type baseResponse struct { + Code int32 `json:"code"` + Message string `json:"msg"` +} + +func (r *baseResponse) GetCode() int32 { + return r.Code +} + +func (r *baseResponse) GetMessage() string { + return r.Message +} + +type loginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type loginResponse struct { + baseResponse + Data *struct { + UserId int64 `json:"user_id"` + Username string `json:"username"` + Token string `json:"token"` + } `json:"data,omitempty"` +} + +type UpdateCertificateRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + SSLPEM string `json:"ssl_pem"` + SSLKey string `json:"ssl_key"` + AutoRenewal bool `json:"auto_renewal"` +} + +type UpdateCertificateResponse struct { + baseResponse +} diff --git a/internal/pkg/sdk3rd/lecdn/v3/master/api.go b/internal/pkg/sdk3rd/lecdn/v3/master/api.go new file mode 100644 index 00000000..00f24a70 --- /dev/null +++ b/internal/pkg/sdk3rd/lecdn/v3/master/api.go @@ -0,0 +1,49 @@ +package master + +import ( + "encoding/json" + "fmt" + "net/http" +) + +func (c *Client) ensureAccessTokenExists() error { + c.accessTokenMtx.Lock() + defer c.accessTokenMtx.Unlock() + if c.accessToken != "" { + return nil + } + + req := &loginRequest{ + Username: c.username, + Password: c.password, + } + res, err := c.sendRequest(http.MethodPost, "/auth/login", req) + if err != nil { + return err + } + + resp := &loginResponse{} + if err := json.Unmarshal(res.Body(), &resp); err != nil { + return fmt.Errorf("lecdn api error: failed to unmarshal response: %w", err) + } else if resp.GetCode() != 200 { + return fmt.Errorf("lecdn get token failed: code='%d', message='%s'", resp.GetCode(), resp.GetMessage()) + } + + c.accessToken = resp.Data.Token + + return nil +} + +func (c *Client) UpdateCertificate(certId int64, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { + if certId == 0 { + return nil, fmt.Errorf("lecdn api error: invalid parameter: CertId") + } + + if err := c.ensureAccessTokenExists(); err != nil { + return nil, err + } + + resp := &UpdateCertificateResponse{} + err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/certificate/%d", certId), req, resp) + return resp, err +} diff --git a/internal/pkg/sdk3rd/lecdn/v3/master/client.go b/internal/pkg/sdk3rd/lecdn/v3/master/client.go new file mode 100644 index 00000000..2f0cae4e --- /dev/null +++ b/internal/pkg/sdk3rd/lecdn/v3/master/client.go @@ -0,0 +1,98 @@ +package master + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "net/http" + "strings" + "sync" + "time" + + "github.com/go-resty/resty/v2" +) + +type Client struct { + apiHost string + username string + password string + + accessToken string + accessTokenMtx sync.Mutex + + client *resty.Client +} + +func NewClient(apiHost, username, password string) *Client { + client := resty.New() + + return &Client{ + apiHost: strings.TrimRight(apiHost, "/"), + username: username, + password: password, + client: client, + } +} + +func (c *Client) WithTimeout(timeout time.Duration) *Client { + c.client.SetTimeout(timeout) + return c +} + +func (c *Client) WithTLSConfig(config *tls.Config) *Client { + c.client.SetTLSClientConfig(config) + return c +} + +func (c *Client) sendRequest(method string, path string, params interface{}) (*resty.Response, error) { + req := c.client.R().SetBasicAuth(c.username, c.password) + if strings.EqualFold(method, http.MethodGet) { + qs := make(map[string]string) + if params != nil { + temp := make(map[string]any) + jsonb, _ := json.Marshal(params) + json.Unmarshal(jsonb, &temp) + for k, v := range temp { + if v != nil { + qs[k] = fmt.Sprintf("%v", v) + } + } + } + + req = req. + SetHeader("Authorization", "Bearer "+c.accessToken). + SetQueryParams(qs) + } else { + req = req. + SetHeader("Content-Type", "application/json"). + SetHeader("Authorization", "Bearer "+c.accessToken). + SetBody(params) + } + + resp, err := req.Execute(method, c.apiHost+"/prod-api"+path) + if err != nil { + return resp, fmt.Errorf("lecdn api error: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("lecdn api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) sendRequestWithResult(method string, path string, params interface{}, result BaseResponse) error { + resp, err := c.sendRequest(method, path, params) + if err != nil { + if resp != nil { + json.Unmarshal(resp.Body(), &result) + } + return err + } + + if err := json.Unmarshal(resp.Body(), &result); err != nil { + return fmt.Errorf("lecdn api error: failed to unmarshal response: %w", err) + } else if errcode := result.GetCode(); errcode != 200 { + return fmt.Errorf("lecdn api error: code='%d', message='%s'", errcode, result.GetMessage()) + } + + return nil +} diff --git a/internal/pkg/sdk3rd/lecdn/v3/master/models.go b/internal/pkg/sdk3rd/lecdn/v3/master/models.go new file mode 100644 index 00000000..2e896f42 --- /dev/null +++ b/internal/pkg/sdk3rd/lecdn/v3/master/models.go @@ -0,0 +1,47 @@ +package master + +type BaseResponse interface { + GetCode() int32 + GetMessage() string +} + +type baseResponse struct { + Code int32 `json:"code"` + Message string `json:"message"` +} + +func (r *baseResponse) GetCode() int32 { + return r.Code +} + +func (r *baseResponse) GetMessage() string { + return r.Message +} + +type loginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type loginResponse struct { + baseResponse + Data *struct { + UserId int64 `json:"user_id"` + Username string `json:"username"` + Token string `json:"token"` + } `json:"data,omitempty"` +} + +type UpdateCertificateRequest struct { + ClientId int64 `json:"client_id"` + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + SSLPEM string `json:"ssl_pem"` + SSLKey string `json:"ssl_key"` + AutoRenewal bool `json:"auto_renewal"` +} + +type UpdateCertificateResponse struct { + baseResponse +} diff --git a/internal/pkg/sdk3rd/upyun/console/api.go b/internal/pkg/sdk3rd/upyun/console/api.go index 32b4626e..ce62d3a6 100644 --- a/internal/pkg/sdk3rd/upyun/console/api.go +++ b/internal/pkg/sdk3rd/upyun/console/api.go @@ -7,7 +7,11 @@ import ( "net/http" ) -func (c *Client) getCookie() error { +func (c *Client) ensureCookieExists() error { + if c.loginCookie != "" { + return nil + } + req := &signinRequest{Username: c.username, Password: c.password} res, err := c.sendRequest(http.MethodPost, "/accounts/signin/", req) if err != nil { @@ -27,10 +31,8 @@ func (c *Client) getCookie() error { } func (c *Client) UploadHttpsCertificate(req *UploadHttpsCertificateRequest) (*UploadHttpsCertificateResponse, error) { - if c.loginCookie == "" { - if err := c.getCookie(); err != nil { - return nil, err - } + if err := c.ensureCookieExists(); err != nil { + return nil, err } resp := &UploadHttpsCertificateResponse{} @@ -39,10 +41,8 @@ func (c *Client) UploadHttpsCertificate(req *UploadHttpsCertificateRequest) (*Up } func (c *Client) GetHttpsCertificateManager(certificateId string) (*GetHttpsCertificateManagerResponse, error) { - if c.loginCookie == "" { - if err := c.getCookie(); err != nil { - return nil, err - } + if err := c.ensureCookieExists(); err != nil { + return nil, err } req := &GetHttpsCertificateManagerRequest{CertificateId: certificateId} @@ -52,10 +52,8 @@ func (c *Client) GetHttpsCertificateManager(certificateId string) (*GetHttpsCert } func (c *Client) UpdateHttpsCertificateManager(req *UpdateHttpsCertificateManagerRequest) (*UpdateHttpsCertificateManagerResponse, error) { - if c.loginCookie == "" { - if err := c.getCookie(); err != nil { - return nil, err - } + if err := c.ensureCookieExists(); err != nil { + return nil, err } resp := &UpdateHttpsCertificateManagerResponse{} @@ -64,10 +62,8 @@ func (c *Client) UpdateHttpsCertificateManager(req *UpdateHttpsCertificateManage } func (c *Client) GetHttpsServiceManager(domain string) (*GetHttpsServiceManagerResponse, error) { - if c.loginCookie == "" { - if err := c.getCookie(); err != nil { - return nil, err - } + if err := c.ensureCookieExists(); err != nil { + return nil, err } req := &GetHttpsServiceManagerRequest{Domain: domain} @@ -77,10 +73,8 @@ func (c *Client) GetHttpsServiceManager(domain string) (*GetHttpsServiceManagerR } func (c *Client) MigrateHttpsDomain(req *MigrateHttpsDomainRequest) (*MigrateHttpsDomainResponse, error) { - if c.loginCookie == "" { - if err := c.getCookie(); err != nil { - return nil, err - } + if err := c.ensureCookieExists(); err != nil { + return nil, err } resp := &MigrateHttpsDomainResponse{} diff --git a/internal/pkg/utils/map/getter.go b/internal/pkg/utils/map/getter.go index f30f6d33..512da3ee 100644 --- a/internal/pkg/utils/map/getter.go +++ b/internal/pkg/utils/map/getter.go @@ -68,31 +68,42 @@ func GetOrDefaultInt32(dict map[string]any, key string, defaultValue int32) int3 } if value, ok := dict[key]; ok { - if result, ok := value.(int32); ok { - if result != 0 { - return result + var result int32 + + switch v := value.(type) { + case int: + result = int32(v) + case int8: + result = int32(v) + case int16: + result = int32(v) + case int32: + result = v + case int64: + result = int32(v) + case uint: + result = int32(v) + case uint8: + result = int32(v) + case uint16: + result = int32(v) + case uint32: + result = int32(v) + case uint64: + result = int32(v) + case float32: + result = int32(v) + case float64: + result = int32(v) + case string: + // 兼容字符串类型的值 + if t, err := strconv.ParseInt(v, 10, 32); err == nil { + result = int32(t) } } - if result, ok := value.(int64); ok { - if result != 0 { - return int32(result) - } - } - - if result, ok := value.(int); ok { - if result != 0 { - return int32(result) - } - } - - // 兼容字符串类型的值 - if str, ok := value.(string); ok { - if result, err := strconv.ParseInt(str, 10, 32); err == nil { - if result != 0 { - return int32(result) - } - } + if result != 0 { + return int32(result) } } @@ -126,31 +137,42 @@ func GetOrDefaultInt64(dict map[string]any, key string, defaultValue int64) int6 } if value, ok := dict[key]; ok { - if result, ok := value.(int64); ok { - if result != 0 { - return result + var result int64 + + switch v := value.(type) { + case int: + result = int64(v) + case int8: + result = int64(v) + case int16: + result = int64(v) + case int32: + result = int64(v) + case int64: + result = v + case uint: + result = int64(v) + case uint8: + result = int64(v) + case uint16: + result = int64(v) + case uint32: + result = int64(v) + case uint64: + result = int64(v) + case float32: + result = int64(v) + case float64: + result = int64(v) + case string: + // 兼容字符串类型的值 + if t, err := strconv.ParseInt(v, 10, 32); err == nil { + result = t } } - if result, ok := value.(int32); ok { - if result != 0 { - return int64(result) - } - } - - if result, ok := value.(int); ok { - if result != 0 { - return int64(result) - } - } - - // 兼容字符串类型的值 - if str, ok := value.(string); ok { - if result, err := strconv.ParseInt(str, 10, 64); err == nil { - if result != 0 { - return result - } - } + if result != 0 { + return int64(result) } } diff --git a/ui/public/imgs/providers/lecdn.svg b/ui/public/imgs/providers/lecdn.svg new file mode 100644 index 00000000..f9c18fa7 --- /dev/null +++ b/ui/public/imgs/providers/lecdn.svg @@ -0,0 +1 @@ +LeCDN \ No newline at end of file diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index 7f3d8e8e..5bc6cab0 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -44,6 +44,7 @@ import AccessFormHuaweiCloudConfig from "./AccessFormHuaweiCloudConfig"; import AccessFormJDCloudConfig from "./AccessFormJDCloudConfig"; import AccessFormKubernetesConfig from "./AccessFormKubernetesConfig"; import AccessFormLarkBotConfig from "./AccessFormLarkBotConfig"; +import AccessFormLeCDNConfig from "./AccessFormLeCDNConfig"; import AccessFormMattermostConfig from "./AccessFormMattermostConfig"; import AccessFormNamecheapConfig from "./AccessFormNamecheapConfig"; import AccessFormNameDotComConfig from "./AccessFormNameDotComConfig"; @@ -243,6 +244,8 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.LARKBOT: return ; + case ACCESS_PROVIDERS.LECDN: + return ; case ACCESS_PROVIDERS.MATTERMOST: return ; case ACCESS_PROVIDERS.NAMECHEAP: diff --git a/ui/src/components/access/AccessFormLeCDNConfig.tsx b/ui/src/components/access/AccessFormLeCDNConfig.tsx new file mode 100644 index 00000000..4af5a639 --- /dev/null +++ b/ui/src/components/access/AccessFormLeCDNConfig.tsx @@ -0,0 +1,85 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input, Radio, Select, Switch } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForLeCDN } from "@/domain/access"; + +type AccessFormLeCDNConfigFieldValues = Nullish; + +export type AccessFormLeCDNConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormLeCDNConfigFieldValues; + onValuesChange?: (values: AccessFormLeCDNConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormLeCDNConfigFieldValues => { + return { + apiUrl: "http://:5090/", + apiVersion: "v3", + apiRole: "user", + username: "", + password: "", + }; +}; + +const AccessFormLeCDNConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormLeCDNConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiUrl: z.string().url(t("common.errmsg.url_invalid")), + role: z.union([z.literal("client"), z.literal("master")], { + message: t("access.form.lecdn_api_role.placeholder"), + }), + username: z.string().nonempty(t("access.form.lecdn_username.placeholder")).trim(), + password: z.string().nonempty(t("access.form.lecdn_password.placeholder")).trim(), + allowInsecureConnections: z.boolean().nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ + + + + + + + + + + + + + + +
+ ); +}; + +export default AccessFormLeCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index ff2f96b3..3bc1774d 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -58,6 +58,7 @@ import DeployNodeConfigFormJDCloudCDNConfig from "./DeployNodeConfigFormJDCloudC import DeployNodeConfigFormJDCloudLiveConfig from "./DeployNodeConfigFormJDCloudLiveConfig"; import DeployNodeConfigFormJDCloudVODConfig from "./DeployNodeConfigFormJDCloudVODConfig"; import DeployNodeConfigFormKubernetesSecretConfig from "./DeployNodeConfigFormKubernetesSecretConfig"; +import DeployNodeConfigFormLeCDNConfig from "./DeployNodeConfigFormLeCDNConfig"; import DeployNodeConfigFormLocalConfig from "./DeployNodeConfigFormLocalConfig"; import DeployNodeConfigFormNetlifySiteConfig from "./DeployNodeConfigFormNetlifySiteConfig"; import DeployNodeConfigFormProxmoxVEConfig from "./DeployNodeConfigFormProxmoxVEConfig"; @@ -266,6 +267,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOYMENT_PROVIDERS.KUBERNETES_SECRET: return ; + case DEPLOYMENT_PROVIDERS.LECDN: + return ; case DEPLOYMENT_PROVIDERS.LOCAL: return ; case DEPLOYMENT_PROVIDERS.NETLIFY_SITE: diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormLeCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormLeCDNConfig.tsx new file mode 100644 index 00000000..0636cedf --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormLeCDNConfig.tsx @@ -0,0 +1,103 @@ +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"; + +type DeployNodeConfigFormLeCDNConfigFieldValues = Nullish<{ + resourceType: string; + certificateId?: string | number; + clientId?: string | number; +}>; + +export type DeployNodeConfigFormLeCDNConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormLeCDNConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormLeCDNConfigFieldValues) => void; +}; + +const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; + +const initFormModel = (): DeployNodeConfigFormLeCDNConfigFieldValues => { + return { + resourceType: RESOURCE_TYPE_CERTIFICATE, + certificateId: "", + clientId: "", + }; +}; + +const DeployNodeConfigFormLeCDNConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormLeCDNConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, { + message: t("workflow_node.deploy.form.lecdn_resource_type.placeholder"), + }), + certificateId: z + .union([z.string(), z.number().int()]) + .nullish() + .refine((v) => { + if (fieldResourceType !== RESOURCE_TYPE_CERTIFICATE) return true; + return /^\d+$/.test(v + "") && +v! > 0; + }, t("workflow_node.deploy.form.lecdn_certificate_id.placeholder")), + clientId: z + .union([z.string(), z.number().int()]) + .nullish() + .refine((v) => { + if (fieldResourceType !== RESOURCE_TYPE_CERTIFICATE) return true; + if (v == null || v === "") return true; + return /^\d+$/.test(v + "") && +v! > 0; + }, t("workflow_node.deploy.form.lecdn_client_id.placeholder")), + }); + const formRule = createSchemaFieldRule(formSchema); + + const fieldResourceType = Form.useWatch("resourceType", formInst); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ + + + + + } + > + + + + } + > + + + +
+ ); +}; + +export default DeployNodeConfigFormLeCDNConfig; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index 48882eb7..4326aa75 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -39,6 +39,7 @@ export interface AccessModel extends BaseModel { | AccessConfigForJDCloud | AccessConfigForKubernetes | AccessConfigForLarkBot + | AccessConfigForLeCDN | AccessConfigForMattermost | AccessConfigForNamecheap | AccessConfigForNameDotCom @@ -248,6 +249,15 @@ export type AccessConfigForLarkBot = { webhookUrl: string; }; +export type AccessConfigForLeCDN = { + apiUrl: string; + apiVersion: string; + apiRole: string; + username: string; + password: string; + allowInsecureConnections?: boolean; +}; + export type AccessConfigForMattermost = { serverUrl: string; username: string; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index a2c432ce..4111f054 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -38,6 +38,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ JDCLOUD: "jdcloud", KUBERNETES: "k8s", LARKBOT: "larkbot", + LECDN: "lecdn", LETSENCRYPT: "letsencrypt", LETSENCRYPTSTAGING: "letsencryptstaging", LOCAL: "local", @@ -129,6 +130,7 @@ export const accessProvidersMap: Maphttps://kb.cachefly.com/kb/guide/en/generating-tokens-and-keys-Oll9Irt5TI/Steps/2460228", - "access.form.cdnfly_api_url.label": "Cdnfly API URL", - "access.form.cdnfly_api_url.placeholder": "Please enter Cdnfly API URL", + "access.form.cdnfly_api_url.label": "Cdnfly URL", + "access.form.cdnfly_api_url.placeholder": "Please enter Cdnfly URL", "access.form.cdnfly_api_key.label": "Cdnfly user API key", "access.form.cdnfly_api_key.placeholder": "Please enter Cdnfly user API key", "access.form.cdnfly_api_key.tooltip": "For more information, see https://doc.cdnfly.cn/shiyongjieshao.html", @@ -193,8 +193,8 @@ "access.form.email_default_sender_address.placeholder": "Please enter default sender email address", "access.form.email_default_receiver_address.label": "Default receiver email address (Optional)", "access.form.email_default_receiver_address.placeholder": "Please enter default receiver email address", - "access.form.flexcdn_api_url.label": "FlexCDN API URL", - "access.form.flexcdn_api_url.placeholder": "Please enter FlexCDN API URL", + "access.form.flexcdn_api_url.label": "FlexCDN URL", + "access.form.flexcdn_api_url.placeholder": "Please enter FlexCDN URL", "access.form.flexcdn_api_role.label": "FlexCDN user role", "access.form.flexcdn_api_role.placeholder": "Please select FlexCDN user role", "access.form.flexcdn_api_role.option.user.label": "Platform user", @@ -223,8 +223,8 @@ "access.form.godaddy_api_secret.label": "GoDaddy API secret", "access.form.godaddy_api_secret.placeholder": "Please enter GoDaddy API secret", "access.form.godaddy_api_secret.tooltip": "For more information, see https://developer.godaddy.com/", - "access.form.goedge_api_url.label": "GoEdge API URL", - "access.form.goedge_api_url.placeholder": "Please enter GoEdge API URL", + "access.form.goedge_api_url.label": "GoEdge URL", + "access.form.goedge_api_url.placeholder": "Please enter GoEdge URL", "access.form.goedge_api_role.label": "GoEdge user role", "access.form.goedge_api_role.placeholder": "Please select GoEdge user role", "access.form.goedge_api_role.option.user.label": "Platform user", @@ -262,6 +262,21 @@ "access.form.larkbot_webhook_url.label": "Lark bot Webhook URL", "access.form.larkbot_webhook_url.placeholder": "Please enter Lark bot Webhook URL", "access.form.larkbot_webhook_url.tooltip": "For more information, see https://www.feishu.cn/hc/en-US/articles/807992406756", + "access.form.lecdn_api_url.label": "LeCDN URL", + "access.form.lecdn_api_url.placeholder": "Please enter LeCDN URL", + "access.form.lecdn_api_version.label": "LeCDN version", + "access.form.lecdn_api_version.placeholder": "Please select LeCDN version", + "access.form.lecdn_api_role.label": "LeCDN user role", + "access.form.lecdn_api_role.placeholder": "Please select LeCDN user role", + "access.form.lecdn_api_role.option.client.label": "Client", + "access.form.lecdn_api_role.option.master.label": "Master", + "access.form.lecdn_username.label": "LeCDN username", + "access.form.lecdn_username.placeholder": "Please enter LeCDN username", + "access.form.lecdn_password.label": "LeCDN password", + "access.form.lecdn_password.placeholder": "Please enter GoEdge password", + "access.form.lecdn_allow_insecure_conns.label": "Insecure SSL/TLS connections", + "access.form.lecdn_allow_insecure_conns.switch.on": "Allow", + "access.form.lecdn_allow_insecure_conns.switch.off": "Disallow", "access.form.mattermost_server_url.label": "Mattermost server URL", "access.form.mattermost_server_url.placeholder": "Please enter Mattermost server URL", "access.form.mattermost_username.label": "Mattermost username", @@ -307,8 +322,8 @@ "access.form.porkbun_secret_api_key.label": "Porkbun secret API key", "access.form.porkbun_secret_api_key.placeholder": "Please enter Porkbun secret API key", "access.form.porkbun_secret_api_key.tooltip": "For more information, see https://porkbun.com/api/json/v3/documentation", - "access.form.powerdns_api_url.label": "PowerDNS API URL", - "access.form.powerdns_api_url.placeholder": "Please enter PowerDNS API URL", + "access.form.powerdns_api_url.label": "PowerDNS URL", + "access.form.powerdns_api_url.placeholder": "Please enter PowerDNS URL", "access.form.powerdns_api_key.label": "PowerDNS API key", "access.form.powerdns_api_key.placeholder": "Please enter PowerDNS API key", "access.form.powerdns_api_key.tooltip": "For more information, see https://doc.powerdns.com/authoritative/http-api/index.html#enabling-the-api", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index 07f727b4..48906a74 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -86,6 +86,7 @@ "provider.kubernetes": "Kubernetes", "provider.kubernetes.secret": "Kubernetes - Secret", "provider.larkbot": "Lark Bot", + "provider.lecdn": "LeCDN", "provider.letsencrypt": "Let's Encrypt", "provider.letsencryptstaging": "Let's Encrypt Staging Environment", "provider.local": "Local deployment", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index ec9a2082..2e3fee98 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -465,6 +465,15 @@ "workflow_node.deploy.form.k8s_secret_data_key_for_key.label": "Kubernetes Secret data key for private key", "workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder": "Please enter Kubernetes Secret data key for private key", "workflow_node.deploy.form.k8s_secret_data_key_for_key.tooltip": "For more information, see https://kubernetes.io/docs/concepts/configuration/secret/", + "workflow_node.deploy.form.lecdn_resource_type.label": "Resource type", + "workflow_node.deploy.form.lecdn_resource_type.placeholder": "Please select resource type", + "workflow_node.deploy.form.lecdn_resource_type.option.certificate.label": "Certificate", + "workflow_node.deploy.form.lecdn_certificate_id.label": "LeCDN certificate ID", + "workflow_node.deploy.form.lecdn_certificate_id.placeholder": "Please enter LeCDN certificate ID", + "workflow_node.deploy.form.lecdn_certificate_id.tooltip": "You can find it on LeCDN WebUI.", + "workflow_node.deploy.form.lecdn_client_id.label": "LeCDN user ID (Optional)", + "workflow_node.deploy.form.lecdn_client_id.placeholder": "Please enter LeCDN user ID", + "workflow_node.deploy.form.lecdn_client_id.tooltip": "You can find it on LeCDN WebUI.

Required when using administrator's authorization. It Must be the same as the user to which the certificate belongs.", "workflow_node.deploy.form.local.guide": "Tips: If you are running Certimate in Docker, the \"Local\" refers to the container rather than the host.", "workflow_node.deploy.form.local_format.label": "File format", "workflow_node.deploy.form.local_format.placeholder": "Please select file format", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 649d44f5..a3d059e1 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -115,8 +115,8 @@ "access.form.cachefly_api_token.label": "CacheFly API Token", "access.form.cachefly_api_token.placeholder": "请输入 CacheFly API Token", "access.form.cachefly_api_token.tooltip": "这是什么?请参阅 https://kb.cachefly.com/kb/guide/en/generating-tokens-and-keys-Oll9Irt5TI/Steps/2460228", - "access.form.cdnfly_api_url.label": "Cdnfly API URL", - "access.form.cdnfly_api_url.placeholder": "请输入 Cdnfly API URL", + "access.form.cdnfly_api_url.label": "Cdnfly URL", + "access.form.cdnfly_api_url.placeholder": "请输入 Cdnfly URL", "access.form.cdnfly_api_key.label": "Cdnfly 用户端 API Key", "access.form.cdnfly_api_key.placeholder": "请输入 Cdnfly 用户端 API Key", "access.form.cdnfly_api_key.tooltip": "这是什么?请参阅 https://doc.cdnfly.cn/shiyongjieshao.html", @@ -187,8 +187,8 @@ "access.form.email_default_sender_address.placeholder": "请输入默认的发送邮箱地址", "access.form.email_default_receiver_address.label": "默认的接收邮箱地址(可选)", "access.form.email_default_receiver_address.placeholder": "请输入默认的接收邮箱地址", - "access.form.flexcdn_api_url.label": "FlexCDN API URL", - "access.form.flexcdn_api_url.placeholder": "请输入 FlexCDN API URL", + "access.form.flexcdn_api_url.label": "FlexCDN URL", + "access.form.flexcdn_api_url.placeholder": "请输入 FlexCDN URL", "access.form.flexcdn_api_role.label": "FlexCDN 用户角色", "access.form.flexcdn_api_role.placeholder": "请选择 FlexCDN 用户角色", "access.form.flexcdn_api_role.option.user.label": "平台用户", @@ -217,8 +217,8 @@ "access.form.godaddy_api_secret.label": "GoDaddy API Secret", "access.form.godaddy_api_secret.placeholder": "请输入 GoDaddy API Secret", "access.form.godaddy_api_secret.tooltip": "这是什么?请参阅 https://developer.godaddy.com/", - "access.form.goedge_api_url.label": "GoEdge API URL", - "access.form.goedge_api_url.placeholder": "请输入 GoEdge API URL", + "access.form.goedge_api_url.label": "GoEdge URL", + "access.form.goedge_api_url.placeholder": "请输入 GoEdge URL", "access.form.goedge_api_role.label": "GoEdge 用户角色", "access.form.goedge_api_role.placeholder": "请选择 GoEdge 用户角色", "access.form.goedge_api_role.option.user.label": "平台用户", @@ -256,6 +256,21 @@ "access.form.larkbot_webhook_url.label": "飞书群机器人 Webhook 地址", "access.form.larkbot_webhook_url.placeholder": "请输入飞书群机器人 Webhook 地址", "access.form.larkbot_webhook_url.tooltip": "这是什么?请参阅 https://www.feishu.cn/hc/zh-CN/articles/807992406756", + "access.form.lecdn_api_url.label": "LeCDN URL", + "access.form.lecdn_api_url.placeholder": "请输入 LeCDN URL", + "access.form.lecdn_api_version.label": "LeCDN 版本", + "access.form.lecdn_api_version.placeholder": "请选择 LeCDN 版本", + "access.form.lecdn_api_role.label": "LeCDN 用户角色", + "access.form.lecdn_api_role.placeholder": "请选择 LeCDN 用户角色", + "access.form.lecdn_api_role.option.client.label": "客户用户", + "access.form.lecdn_api_role.option.master.label": "主控管理员", + "access.form.lecdn_username.label": "LeCDN 用户名", + "access.form.lecdn_username.placeholder": "请输入 LeCDN 用户名", + "access.form.lecdn_password.label": "LeCDN 用户密码", + "access.form.lecdn_password.placeholder": "请输入 LeCDN 用户密码", + "access.form.lecdn_allow_insecure_conns.label": "忽略 SSL/TLS 证书错误", + "access.form.lecdn_allow_insecure_conns.switch.on": "允许", + "access.form.lecdn_allow_insecure_conns.switch.off": "不允许", "access.form.mattermost_server_url.label": "Mattermost 服务地址", "access.form.mattermost_server_url.placeholder": "请输入 Mattermost 服务地址", "access.form.mattermost_username.label": "Mattermost 用户名", @@ -301,8 +316,8 @@ "access.form.porkbun_secret_api_key.label": "Porkbun Secret API Key", "access.form.porkbun_secret_api_key.placeholder": "请输入 Porkbun Secret API Key", "access.form.porkbun_secret_api_key.tooltip": "这是什么?请参阅 https://porkbun.com/api/json/v3/documentation", - "access.form.powerdns_api_url.label": "PowerDNS API URL", - "access.form.powerdns_api_url.placeholder": "请输入 PowerDNS API URL", + "access.form.powerdns_api_url.label": "PowerDNS URL", + "access.form.powerdns_api_url.placeholder": "请输入 PowerDNS URL", "access.form.powerdns_api_key.label": "PowerDNS API Key", "access.form.powerdns_api_key.placeholder": "请输入 PowerDNS API Key", "access.form.powerdns_api_key.tooltip": "这是什么?请参阅 https://doc.powerdns.com/authoritative/http-api/index.html#enabling-the-api", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 45f3fd53..9d997ca0 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -86,6 +86,7 @@ "provider.kubernetes": "Kubernetes", "provider.kubernetes.secret": "Kubernetes - Secret", "provider.larkbot": "飞书群机器人", + "provider.lecdn": "LeCDN", "provider.letsencrypt": "Let's Encrypt", "provider.letsencryptstaging": "Let's Encrypt 测试环境", "provider.local": "本地部署", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index ff2f0522..356c3d64 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -109,18 +109,18 @@ "workflow_node.deploy.form.certificate.tooltip": "待部署证书来自之前的申请或上传节点。如果选项为空请先确保前序节点配置正确。", "workflow_node.deploy.form.params_config.label": "参数设置", "workflow_node.deploy.form.1panel_console_auto_restart.label": "部署后自动重启宝塔面板服务", - "workflow_node.deploy.form.1panel_site_resource_type.label": "证书替换方式", - "workflow_node.deploy.form.1panel_site_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.1panel_site_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.1panel_site_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.1panel_site_resource_type.option.website.label": "替换指定网站的证书", "workflow_node.deploy.form.1panel_site_resource_type.option.certificate.label": "替换指定证书", "workflow_node.deploy.form.1panel_site_website_id.label": "1Panel 网站 ID", "workflow_node.deploy.form.1panel_site_website_id.placeholder": "请输入 1Panel 网站 ID", - "workflow_node.deploy.form.1panel_site_website_id.tooltip": "请登录 1Panel 管理面板查看。", + "workflow_node.deploy.form.1panel_site_website_id.tooltip": "请登录 1Panel 面板查看。", "workflow_node.deploy.form.1panel_site_certificate_id.label": "1Panel 证书 ID", "workflow_node.deploy.form.1panel_site_certificate_id.placeholder": "请输入 1Panel 证书 ID", - "workflow_node.deploy.form.1panel_site_certificate_id.tooltip": "请登录 1Panel 管理面板查看。", - "workflow_node.deploy.form.aliyun_alb_resource_type.label": "证书替换方式", - "workflow_node.deploy.form.aliyun_alb_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.1panel_site_certificate_id.tooltip": "请登录 1Panel 面板查看。", + "workflow_node.deploy.form.aliyun_alb_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.aliyun_alb_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.aliyun_alb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS/QUIC 监听的证书", "workflow_node.deploy.form.aliyun_alb_resource_type.option.listener.label": "替换指定负载均衡监听器的证书", "workflow_node.deploy.form.aliyun_alb_region.label": "阿里云 ALB 服务地域", @@ -170,8 +170,8 @@ "workflow_node.deploy.form.aliyun_cas_deploy_contact_ids.tooltip": "这是什么?请参阅 https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-listcontact

不填写时,将使用系统联系人列表中的第一个。", "workflow_node.deploy.form.aliyun_cas_deploy_contact_ids.multiple_input_modal.title": "修改阿里云联系人 ID", "workflow_node.deploy.form.aliyun_cas_deploy_contact_ids.multiple_input_modal.placeholder": "请输入阿里云联系人 ID", - "workflow_node.deploy.form.aliyun_clb_resource_type.label": "证书替换方式", - "workflow_node.deploy.form.aliyun_clb_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.aliyun_clb_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.aliyun_clb_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.aliyun_clb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS 监听的证书", "workflow_node.deploy.form.aliyun_clb_resource_type.option.listener.label": "替换指定负载均衡监听的证书", "workflow_node.deploy.form.aliyun_clb_region.label": "阿里云 CLB 服务地域", @@ -212,8 +212,8 @@ "workflow_node.deploy.form.aliyun_fc_domain.label": "阿里云 FC 自定义域名", "workflow_node.deploy.form.aliyun_fc_domain.placeholder": "请输入阿里云 FC 自定义域名(支持泛域名)", "workflow_node.deploy.form.aliyun_fc_domain.tooltip": "这是什么?请参阅 see https://fcnext.console.aliyun.com/", - "workflow_node.deploy.form.aliyun_ga_resource_type.label": "证书替换方式", - "workflow_node.deploy.form.aliyun_ga_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.aliyun_ga_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.aliyun_ga_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.aliyun_ga_resource_type.option.accelerator.label": "替换指定全球加速器下的全部 HTTPS 监听的证书", "workflow_node.deploy.form.aliyun_ga_resource_type.option.listener.label": "替换指定全球加速器监听器的证书", "workflow_node.deploy.form.aliyun_ga_accelerator_id.label": "阿里云全球加速实例 ID", @@ -231,8 +231,8 @@ "workflow_node.deploy.form.aliyun_live_domain.label": "阿里云视频直播流域名", "workflow_node.deploy.form.aliyun_live_domain.placeholder": "请输入阿里云视频直播流域名(支持泛域名)", "workflow_node.deploy.form.aliyun_live_domain.tooltip": "这是什么?请参阅 https://live.console.aliyun.com", - "workflow_node.deploy.form.aliyun_nlb_resource_type.label": "证书替换方式", - "workflow_node.deploy.form.aliyun_nlb_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.aliyun_nlb_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.aliyun_nlb_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.aliyun_nlb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS/QUIC 监听的证书", "workflow_node.deploy.form.aliyun_nlb_resource_type.option.listener.label": "替换指定负载均衡监听器的证书", "workflow_node.deploy.form.aliyun_nlb_region.label": "阿里云 NLB 服务地域", @@ -289,8 +289,8 @@ "workflow_node.deploy.form.azure_keyvault_certificate_name.placeholder": "请输入 Azure KeyVault 证书名称", "workflow_node.deploy.form.azure_keyvault_certificate_name.tooltip": "不填写时,将由 Certimate 自动生成证书名称。", "workflow_node.deploy.form.azure_keyvault_certificate_name.errmsg.invalid": "证书名称只能包含字母、数字和连字符(-),长度限制为 1 到 127 个字符", - "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.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 服务地域", @@ -305,8 +305,8 @@ "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": "这是什么?请参阅 https://console.bce.baidu.com/blb/#/appblb/list

不填写时,将替换监听器的默认证书;否则,将替换扩展域名证书。", - "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.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 服务地域", @@ -353,48 +353,48 @@ "workflow_node.deploy.form.byteplus_cdn_domain.label": "BytePlus CDN 域名", "workflow_node.deploy.form.byteplus_cdn_domain.placeholder": "请输入 BytePlus CDN 域名(支持泛域名)", "workflow_node.deploy.form.byteplus_cdn_domain.tooltip": "这是什么?请参阅 https://console.byteplus.com/cdn", - "workflow_node.deploy.form.cdnfly_resource_type.label": "证书替换方式", - "workflow_node.deploy.form.cdnfly_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.cdnfly_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.cdnfly_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.cdnfly_resource_type.option.site.label": "替换指定网站的证书", "workflow_node.deploy.form.cdnfly_resource_type.option.certificate.label": "替换指定证书", "workflow_node.deploy.form.cdnfly_site_id.label": "Cdnfly 网站 ID", "workflow_node.deploy.form.cdnfly_site_id.placeholder": "请输入 Cdnfly 网站 ID", - "workflow_node.deploy.form.cdnfly_site_id.tooltip": "请登录 Cdnfly 管理平台查看。", + "workflow_node.deploy.form.cdnfly_site_id.tooltip": "请登录 Cdnfly 控制台查看。", "workflow_node.deploy.form.cdnfly_certificate_id.label": "Cdnfly 证书 ID", "workflow_node.deploy.form.cdnfly_certificate_id.placeholder": "请输入 Cdnfly 证书 ID", - "workflow_node.deploy.form.cdnfly_certificate_id.tooltip": "请登录 Cdnfly 管理平台查看。", + "workflow_node.deploy.form.cdnfly_certificate_id.tooltip": "请登录 Cdnfly 控制台查看。", "workflow_node.deploy.form.dogecloud_cdn_domain.label": "多吉云 CDN 加速域名", "workflow_node.deploy.form.dogecloud_cdn_domain.placeholder": "请输入多吉云 CDN 加速域名", "workflow_node.deploy.form.dogecloud_cdn_domain.tooltip": "这是什么?请参阅 https://console.dogecloud.com", "workflow_node.deploy.form.edgio_applications_environment_id.label": "Edgio Applications 环境 ID", "workflow_node.deploy.form.edgio_applications_environment_id.placeholder": "请输入 Edgio Applications 环境 ID", "workflow_node.deploy.form.edgio_applications_environment_id.tooltip": "这是什么?请参阅 https://edgio.app/", - "workflow_node.deploy.form.flexcdn_resource_type.label": "证书替换方式", - "workflow_node.deploy.form.flexcdn_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.flexcdn_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.flexcdn_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.flexcdn_resource_type.option.certificate.label": "替换指定证书", "workflow_node.deploy.form.flexcdn_certificate_id.label": "FlexCDN 证书 ID", "workflow_node.deploy.form.flexcdn_certificate_id.placeholder": "请输入 FlexCDN 证书 ID", - "workflow_node.deploy.form.flexcdn_certificate_id.tooltip": "请登录 FlexCDN 管理平台查看。", + "workflow_node.deploy.form.flexcdn_certificate_id.tooltip": "请登录 FlexCDN 控制台查看。", "workflow_node.deploy.form.gcore_cdn_resource_id.label": "Gcore CDN 资源 ID", "workflow_node.deploy.form.gcore_cdn_resource_id.placeholder": "请输入 Gcore CDN 资源 ID", "workflow_node.deploy.form.gcore_cdn_resource_id.tooltip": "这是什么?请参阅 https://cdn.gcore.com/resources/list", "workflow_node.deploy.form.gcore_cdn_certificate_id.label": "Gcore CDN 原证书 ID(可选)", "workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder": "请输入 Gcore CDN 原证书 ID", "workflow_node.deploy.form.gcore_cdn_certificate_id.tooltip": "这是什么?请参阅 https://cdn.gcore.com/ssl

不填写时,将上传新证书;否则,将替换原证书。", - "workflow_node.deploy.form.goedge_resource_type.label": "证书替换方式", - "workflow_node.deploy.form.goedge_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.goedge_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.goedge_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.goedge_resource_type.option.certificate.label": "替换指定证书", "workflow_node.deploy.form.goedge_certificate_id.label": "GoEdge 证书 ID", "workflow_node.deploy.form.goedge_certificate_id.placeholder": "请输入 GoEdge 证书 ID", - "workflow_node.deploy.form.goedge_certificate_id.tooltip": "请登录 GoEdge 管理平台查看。", + "workflow_node.deploy.form.goedge_certificate_id.tooltip": "请登录 GoEdge 控制台查看。", "workflow_node.deploy.form.huaweicloud_cdn_region.label": "华为云 CDN 服务区域", "workflow_node.deploy.form.huaweicloud_cdn_region.placeholder": "请输入华为云 CDN 服务区域(例如:cn-north-1)", "workflow_node.deploy.form.huaweicloud_cdn_region.tooltip": "这是什么?请参阅 https://console.huaweicloud.com/apiexplorer/#/endpoint", "workflow_node.deploy.form.huaweicloud_cdn_domain.label": "华为云 CDN 加速域名", "workflow_node.deploy.form.huaweicloud_cdn_domain.placeholder": "请输入华为云 CDN 加速域名", "workflow_node.deploy.form.huaweicloud_cdn_domain.tooltip": "这是什么?请参阅 https://console.huaweicloud.com/cdn", - "workflow_node.deploy.form.huaweicloud_elb_resource_type.label": "证书替换方式", - "workflow_node.deploy.form.huaweicloud_elb_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.huaweicloud_elb_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.huaweicloud_elb_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.huaweicloud_elb_resource_type.option.certificate.label": "替换指定证书", "workflow_node.deploy.form.huaweicloud_elb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS 监听器的证书", "workflow_node.deploy.form.huaweicloud_elb_resource_type.option.listener.label": "替换指定监听器的证书", @@ -410,8 +410,8 @@ "workflow_node.deploy.form.huaweicloud_elb_listener_id.label": "华为云 ELB 监听器 ID", "workflow_node.deploy.form.huaweicloud_elb_listener_id.placeholder": "请输入华为云 ELB 监听器 ID", "workflow_node.deploy.form.huaweicloud_elb_listener_id.tooltip": "这是什么?请参阅 https://console.huaweicloud.com/vpc/#/elb/list/grid", - "workflow_node.deploy.form.huaweicloud_waf_resource_type.label": "证书替换方式", - "workflow_node.deploy.form.huaweicloud_waf_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.huaweicloud_waf_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.huaweicloud_waf_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.huaweicloud_waf_resource_type.option.certificate.label": "替换指定证书", "workflow_node.deploy.form.huaweicloud_waf_resource_type.option.cloudserver.label": "替换指定云模式防护网站的证书", "workflow_node.deploy.form.huaweicloud_waf_resource_type.option.premiumhost.label": "替换指定独享模式防护网站的证书", @@ -424,8 +424,8 @@ "workflow_node.deploy.form.huaweicloud_waf_domain.label": "华为云 WAF 防护域名", "workflow_node.deploy.form.huaweicloud_waf_domain.placeholder": "请输入华为云 WAF 防护域名(支持泛域名)", "workflow_node.deploy.form.huaweicloud_waf_domain.tooltip": "这是什么?请参阅 https://console.huaweicloud.com/console/#/waf/domain/list", - "workflow_node.deploy.form.jdcloud_alb_resource_type.label": "证书替换方式", - "workflow_node.deploy.form.jdcloud_alb_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.jdcloud_alb_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.jdcloud_alb_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.jdcloud_alb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS/TLS 监听的证书", "workflow_node.deploy.form.jdcloud_alb_resource_type.option.listener.label": "替换指定负载均衡监听器的证书", "workflow_node.deploy.form.jdcloud_alb_region_id.label": "京东云 ALB 服务地域 ID", @@ -464,6 +464,15 @@ "workflow_node.deploy.form.k8s_secret_data_key_for_key.label": "Kubernetes Secret 数据键(用于存放私钥的字段)", "workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder": "请输入 Kubernetes Secret 中用于存放私钥的数据键", "workflow_node.deploy.form.k8s_secret_data_key_for_key.tooltip": "这是什么?请参阅 https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/", + "workflow_node.deploy.form.lecdn_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.lecdn_resource_type.placeholder": "请选择证书部署方式", + "workflow_node.deploy.form.lecdn_resource_type.option.certificate.label": "替换指定证书", + "workflow_node.deploy.form.lecdn_certificate_id.label": "LeCDN 证书 ID", + "workflow_node.deploy.form.lecdn_certificate_id.placeholder": "请输入 LeCDN 证书 ID", + "workflow_node.deploy.form.lecdn_certificate_id.tooltip": "请登录 LeCDN 控制台查看。", + "workflow_node.deploy.form.lecdn_client_id.label": "LeCDN 客户 ID(可选)", + "workflow_node.deploy.form.lecdn_client_id.placeholder": "请输入 LeCDN 客户 ID", + "workflow_node.deploy.form.lecdn_client_id.tooltip": "请登录 LeCDN 控制台查看。

使用的是系统管理员的授权信息时必填,需与证书所属客户相同。", "workflow_node.deploy.form.local.guide": "小贴士:如果你正在使用 Docker 运行 Certimate,“本地”指的是容器内而非宿主机。", "workflow_node.deploy.form.local_format.label": "文件格式", "workflow_node.deploy.form.local_format.placeholder": "请选择文件格式", @@ -537,8 +546,8 @@ "workflow_node.deploy.form.ratpanel_site_name.label": "耗子面板网站名称", "workflow_node.deploy.form.ratpanel_site_name.placeholder": "请输入耗子面板网站名称", "workflow_node.deploy.form.ratpanel_site_name.tooltip": "请登录耗子面板查看。", - "workflow_node.deploy.form.safeline_resource_type.label": "证书替换方式", - "workflow_node.deploy.form.safeline_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.safeline_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.safeline_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.safeline_resource_type.option.certificate.label": "替换指定证书", "workflow_node.deploy.form.safeline_certificate_id.label": "雷池证书 ID", "workflow_node.deploy.form.safeline_certificate_id.placeholder": "请输入雷池证书 ID", @@ -590,8 +599,8 @@ "workflow_node.deploy.form.tencentcloud_cdn_domain.label": "腾讯云 CDN 加速域名", "workflow_node.deploy.form.tencentcloud_cdn_domain.placeholder": "请输入腾讯云 CDN 加速域名(支持泛域名)", "workflow_node.deploy.form.tencentcloud_cdn_domain.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/cdn", - "workflow_node.deploy.form.tencentcloud_clb_resource_type.label": "证书替换方式", - "workflow_node.deploy.form.tencentcloud_clb_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.tencentcloud_clb_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.tencentcloud_clb_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.tencentcloud_clb_resource_type.option.ssl_deploy.label": "通过 SSL 服务部署到云资源实例", "workflow_node.deploy.form.tencentcloud_clb_resource_type.option.loadbalancer.label": "替换指定实例下的全部 HTTPS/TCPSSL/QUIC 监听器的证书", "workflow_node.deploy.form.tencentcloud_clb_resource_type.option.listener.label": "替换指定监听器的证书", @@ -687,8 +696,8 @@ "workflow_node.deploy.form.upyun_file_domain.label": "又拍云云存储加速域名", "workflow_node.deploy.form.upyun_file_domain.placeholder": "请输入又拍云云存储加速域名", "workflow_node.deploy.form.upyun_file_domain.tooltip": "这是什么?请参阅 https://console.upyun.com/services/file/", - "workflow_node.deploy.form.volcengine_alb_resource_type.label": "证书替换方式", - "workflow_node.deploy.form.volcengine_alb_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.volcengine_alb_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.volcengine_alb_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.volcengine_alb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS 监听的证书", "workflow_node.deploy.form.volcengine_alb_resource_type.option.listener.label": "替换指定监听器的证书", "workflow_node.deploy.form.volcengine_alb_region.label": "火山引擎 ALB 服务地域", @@ -708,8 +717,8 @@ "workflow_node.deploy.form.volcengine_cdn_domain.tooltip": "这是什么?请参阅 https://console.volcengine.com/cdn/homepage", "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.label": "证书部署方式", + "workflow_node.deploy.form.volcengine_clb_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.volcengine_clb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS 监听的证书", "workflow_node.deploy.form.volcengine_clb_resource_type.option.listener.label": "替换指定监听器的证书", "workflow_node.deploy.form.volcengine_clb_region.label": "火山引擎 CLB 服务地域",