diff --git a/internal/domain/provider.go b/internal/domain/provider.go index fec6d3bf..4562fda9 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -252,6 +252,7 @@ const ( DeploymentProviderTypeHuaweiCloudCDN = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-cdn") DeploymentProviderTypeHuaweiCloudELB = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-elb") DeploymentProviderTypeHuaweiCloudSCM = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-scm") + DeploymentProviderTypeHuaweiCloudOBS = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-obs") DeploymentProviderTypeHuaweiCloudWAF = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-waf") DeploymentProviderTypeJDCloudALB = DeploymentProviderType(AccessProviderTypeJDCloud + "-alb") DeploymentProviderTypeJDCloudCDN = DeploymentProviderType(AccessProviderTypeJDCloud + "-cdn") diff --git a/pkg/core/ssl-deployer/providers/huaweicloud-obs/huaweicloud_obs.go b/pkg/core/ssl-deployer/providers/huaweicloud-obs/huaweicloud_obs.go new file mode 100644 index 00000000..6e8c2fa5 --- /dev/null +++ b/pkg/core/ssl-deployer/providers/huaweicloud-obs/huaweicloud_obs.go @@ -0,0 +1,124 @@ +package huaweicloudobs + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/md5" + "crypto/sha1" + "encoding/base64" + "errors" + "fmt" + "log/slog" + "net/http" + "strings" + "time" + + "github.com/certimate-go/certimate/pkg/core" +) + +type SSLDeployerProviderConfig struct { + // 华为云 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // 华为云 SecretAccessKey。 + SecretAccessKey string `json:"secretAccessKey"` + // 华为云 Bucket 对应的 Endpoint。 + Endpoint string `json:"endpoint"` + // 华为云 OBS 桶名。 + Bucket string `json:"bucket"` + // 自定义域名。 + Domain string `json:"domain"` +} + +type SSLDeployerProvider struct { + config *SSLDeployerProviderConfig + logger *slog.Logger +} + +var _ core.SSLDeployer = (*SSLDeployerProvider)(nil) + +func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) { + if config == nil { + return nil, errors.New("the configuration of the ssl deployer provider is nil") + } + + config.Endpoint = strings.TrimPrefix(strings.TrimPrefix(config.Endpoint, "http://"), "https://") + + return &SSLDeployerProvider{ + config: config, + logger: slog.Default(), + }, nil +} + +func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) { + if logger == nil { + d.logger = slog.New(slog.DiscardHandler) + } else { + d.logger = logger + } +} + +// REF: https://support.huaweicloud.com/usermanual-obs/obs_06_3200.html +// REF: https://support.huaweicloud.com/api-obs/obs_04_0059.html +func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) { + if d.config.Domain == "" { + return nil, fmt.Errorf("config `domain` is required") + } + + url := fmt.Sprintf("https://%s.%s/?customdomain=%s", d.config.Bucket, d.config.Endpoint, d.config.Domain) + bodyXML := fmt.Sprintf(` + + %s + %s + %s + %s +`, + d.config.Bucket+"_"+d.config.Domain, certPEM, certPEM, privkeyPEM, + ) + + // 计算 Content-MD5(Base64 编码) + md5sum := md5.Sum([]byte(bodyXML)) + contentMD5 := base64.StdEncoding.EncodeToString(md5sum[:]) + + // 日期 + date := time.Now().UTC().Format(http.TimeFormat) + + // 构造签名字符串 + method := "PUT" + contentType := "application/xml" + canonicalizedResource := fmt.Sprintf("/%s/?customdomain=%s", d.config.Bucket, d.config.Domain) + stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s", method, contentMD5, contentType, date, canonicalizedResource) + + // HMAC-SHA1 签名 + h := hmac.New(sha1.New, []byte(d.config.SecretAccessKey)) + h.Write([]byte(stringToSign)) + signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) + + // Authorization + authHeader := fmt.Sprintf("OBS %s:%s", d.config.AccessKeyId, signature) + + // 创建请求 + req, err := http.NewRequest(method, url, bytes.NewBuffer([]byte(bodyXML))) + if err != nil { + return nil, err + } + + req.Header.Set("Date", date) + req.Header.Set("Authorization", authHeader) + req.Header.Set("Content-MD5", contentMD5) + req.Header.Set("Content-Type", contentType) + + // 请求 + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body := new(bytes.Buffer) + body.ReadFrom(resp.Body) + return nil, fmt.Errorf("HTTP request failed with status %d: %s", resp.StatusCode, body.String()) + } + return &core.SSLDeployResult{}, nil +} diff --git a/pkg/core/ssl-deployer/providers/huaweicloud-obs/huaweicloud_obs_test.go b/pkg/core/ssl-deployer/providers/huaweicloud-obs/huaweicloud_obs_test.go new file mode 100644 index 00000000..a1d06f00 --- /dev/null +++ b/pkg/core/ssl-deployer/providers/huaweicloud-obs/huaweicloud_obs_test.go @@ -0,0 +1,85 @@ +package huaweicloudobs_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/huaweicloud-obs" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fSecretAccessKey string + fEndpoint string + fBucket string + fDomain string +) + +func init() { + argsPrefix := "CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") + flag.StringVar(&fEndpoint, argsPrefix+"ENDPOINT", "", "") + flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "") + flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") +} + +/* +Shell command to run this test: + + go test -v ./huaweicloud_obs_test.go -args \ + --CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \ + --CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_ENDPOINT="https://your-endpoint" \ + --CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_BUCKET="your-bucket" \ + --CERTIMATE_SSLDEPLOYER_HUAWEICLOUDCDN_DOMAIN="example.com" +*/ +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("ENDPOINT: %v", fEndpoint), + fmt.Sprintf("BUCKET: %v", fBucket), + fmt.Sprintf("DOMAIN: %v", fDomain), + }, "\n")) + + deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{ + AccessKeyId: fAccessKeyId, + SecretAccessKey: fSecretAccessKey, + Endpoint: fEndpoint, + Bucket: fBucket, + 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) + }) +} diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderHuaweiCloudOBS.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderHuaweiCloudOBS.tsx new file mode 100644 index 00000000..4406cf1f --- /dev/null +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderHuaweiCloudOBS.tsx @@ -0,0 +1,84 @@ +import { getI18n, useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { validDomainName } from "@/utils/validators"; + +import { useFormNestedFieldsContext } from "./_context"; + +const BizDeployNodeConfigFieldsProviderHuaweiCloudOBS = () => { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const initialValues = getInitialValues(); + + return ( + <> + } + > + + + + } + > + + + + } + > + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + endpoint: "", + bucket: "", + domain: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { + const { t } = i18n; + + return z.object({ + endpoint: z + .string(t("workflow_node.deploy.form.huaweicloud_obs_endpoint.placeholder")) + .nonempty(t("workflow_node.deploy.form.huaweicloud_obs_endpoint.placeholder")), + bucket: z + .string(t("workflow_node.deploy.form.huaweicloud_obs_bucket.placeholder")) + .nonempty(t("workflow_node.deploy.form.huaweicloud_obs_bucket.placeholder")), + domain: z + .string(t("workflow_node.deploy.form.huaweicloud_obs_domain.placeholder")) + .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), + }); +}; + +const _default = Object.assign(BizDeployNodeConfigFieldsProviderHuaweiCloudOBS, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigForm.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigForm.tsx index 005618d2..6cd8f9f7 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigForm.tsx @@ -64,6 +64,7 @@ import BizDeployNodeConfigFieldsProviderGcoreCDN from "./BizDeployNodeConfigFiel import BizDeployNodeConfigFieldsProviderGoEdge from "./BizDeployNodeConfigFieldsProviderGoEdge"; import BizDeployNodeConfigFieldsProviderHuaweiCloudCDN from "./BizDeployNodeConfigFieldsProviderHuaweiCloudCDN"; import BizDeployNodeConfigFieldsProviderHuaweiCloudELB from "./BizDeployNodeConfigFieldsProviderHuaweiCloudELB"; +import BizDeployNodeConfigFieldsProviderHuaweiCloudOBS from "./BizDeployNodeConfigFieldsProviderHuaweiCloudOBS"; import BizDeployNodeConfigFieldsProviderHuaweiCloudWAF from "./BizDeployNodeConfigFieldsProviderHuaweiCloudWAF"; import BizDeployNodeConfigFieldsProviderJDCloudALB from "./BizDeployNodeConfigFieldsProviderJDCloudALB"; import BizDeployNodeConfigFieldsProviderJDCloudCDN from "./BizDeployNodeConfigFieldsProviderJDCloudCDN"; @@ -322,6 +323,9 @@ const BizDeployNodeConfigForm = ({ node, ...props }: BizDeployNodeConfigFormProp case DEPLOYMENT_PROVIDERS.HUAWEICLOUD_ELB: { return BizDeployNodeConfigFieldsProviderHuaweiCloudELB; } + case DEPLOYMENT_PROVIDERS.HUAWEICLOUD_OBS: { + return BizDeployNodeConfigFieldsProviderHuaweiCloudOBS; + } case DEPLOYMENT_PROVIDERS.HUAWEICLOUD_WAF: { return BizDeployNodeConfigFieldsProviderHuaweiCloudWAF; } diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 8a934aa9..e5370d3c 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -485,6 +485,7 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({ HUAWEICLOUD_CDN: `${ACCESS_PROVIDERS.HUAWEICLOUD}-cdn`, HUAWEICLOUD_ELB: `${ACCESS_PROVIDERS.HUAWEICLOUD}-elb`, HUAWEICLOUD_SCM: `${ACCESS_PROVIDERS.HUAWEICLOUD}-scm`, + HUAWEICLOUD_OBS: `${ACCESS_PROVIDERS.HUAWEICLOUD}-obs`, HUAWEICLOUD_WAF: `${ACCESS_PROVIDERS.HUAWEICLOUD}-waf`, JDCLOUD_ALB: `${ACCESS_PROVIDERS.JDCLOUD}-alb`, JDCLOUD_CDN: `${ACCESS_PROVIDERS.JDCLOUD}-cdn`, @@ -605,6 +606,7 @@ export const deploymentProvidersMap: Mapcrontab rules.", "workflow_node.start.form.trigger_cron.help": "Expected execution time for the last 5 times (the actual time zone is based on the server):", "workflow_node.start.form.trigger_cron.guide": "If you have multiple workflows, it is recommended to set them to run at different times of the day instead of always running at a specific time. And please don't always set it to midnight every day to avoid spikes in traffic.

Reference links:
1. Let’s Encrypt rate limits
2. Why should my Let’s Encrypt (ACME) client run at a random time?", - "workflow_node.apply.label": "Request certificate", "workflow_node.apply.help": "Apply for SSL certificate issuance from the certificate authority.", "workflow_node.apply.default_name": "Application", @@ -126,7 +125,6 @@ "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 issued certificate, otherwise the certificate may never be renewed.", - "workflow_node.upload.label": "Upload certificate", "workflow_node.upload.help": "Upload the user's existing SSL certificate.", "workflow_node.upload.default_name": "Uploading", @@ -137,7 +135,6 @@ "workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----", "workflow_node.upload.form.private_key.label": "Private key (PEM format)", "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----", - "workflow_node.monitor.label": "Monitor certificate", "workflow_node.monitor.help": "Obtain the SSL certificate of the website through HTTPS protocol.", "workflow_node.monitor.default_name": "Monitoring", @@ -152,7 +149,6 @@ "workflow_node.monitor.form.domain.help": "Notes: It is only required when the host is an IP address.", "workflow_node.monitor.form.request_path.label": "Request path (Optional)", "workflow_node.monitor.form.request_path.placeholder": "Please enter request path", - "workflow_node.deploy.label": "Deploy certificate", "workflow_node.deploy.help": "Invoke the APIs of the service provider to deploy the SSL certificate.", "workflow_node.deploy.default_name": "Deployment", @@ -522,6 +518,15 @@ "workflow_node.deploy.form.huaweicloud_cdn_domain.label": "Huawei Cloud CDN domain", "workflow_node.deploy.form.huaweicloud_cdn_domain.placeholder": "Please enter Huawei Cloud CDN domain name", "workflow_node.deploy.form.huaweicloud_cdn_domain.tooltip": "For more information, see https://console-intl.huaweicloud.com/cdn", + "workflow_node.deploy.form.huaweicloud_obs_endpoint.label": "Huawei Cloud OBS Endpoint", + "workflow_node.deploy.form.huaweicloud_obs_endpoint.placeholder": "Please enter the Huawei Cloud OBS endpoint (e.g., obs.cn-north-4.myhuaweicloud.com)", + "workflow_node.deploy.form.huaweicloud_obs_endpoint.tooltip": "What is this? Please refer to https://support.huaweicloud.com/productdesc-obs/obs_03_0152.html", + "workflow_node.deploy.form.huaweicloud_obs_bucket.label": "Huawei Cloud COS Bucket Name", + "workflow_node.deploy.form.huaweicloud_obs_bucket.placeholder": "Please enter the Huawei Cloud COS bucket name", + "workflow_node.deploy.form.huaweicloud_obs_bucket.tooltip": "What is this? Please refer to https://support.huaweicloud.com/productdesc-obs/obs_03_0207.html", + "workflow_node.deploy.form.huaweicloud_obs_domain.label": "Huawei Cloud COS Custom Domain", + "workflow_node.deploy.form.huaweicloud_obs_domain.placeholder": "Please enter the Huawei Cloud COS custom domain", + "workflow_node.deploy.form.huaweicloud_obs_domain.tooltip": "What is this? Please refer to https://support.huaweicloud.com/productdesc-obs/obs_03_0207.html", "workflow_node.deploy.form.huaweicloud_elb_region.label": "Huawei Cloud ELB region", "workflow_node.deploy.form.huaweicloud_elb_region.placeholder": "Please enter Huawei Cloud ELB region (e.g. cn-north-1)", "workflow_node.deploy.form.huaweicloud_elb_region.tooltip": "For more information, see https://console-intl.huaweicloud.com/apiexplorer/#/endpoint", @@ -1021,7 +1026,6 @@ "workflow_node.deploy.form.skip_on_last_succeeded.suffix": " to re-deploy.", "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": "Send notification", "workflow_node.notify.help": "Invoke the APIs of the service provider to push message notifications.", "workflow_node.notify.default_name": "Notification", @@ -1083,7 +1087,6 @@ "workflow_node.condition.default_name": "Parallel", "workflow_node.condition.default_name.template_certtest_on_expire_soon": "If the certificate will be expiring soon ...", "workflow_node.condition.default_name.template_certtest_on_expired": "If the certificate has expired ...", - "workflow_node.branch_block.label": "Branch", "workflow_node.branch_block.default_name": "Branch", "workflow_node.branch_block.state.no": "Enter Unconditionally", @@ -1112,7 +1115,6 @@ "workflow_node.branch_block.form.expression.value.option.true.label": "True", "workflow_node.branch_block.form.expression.value.option.false.label": "False", "workflow_node.branch_block.form.expression.add_condition.button": "Add condition", - "workflow_node.try_catch.label": "Execution result branch", "workflow_node.try_catch.help": "Attempt to execute subsequent nodes, and when any node fails to execute, interrupt and enter the execution failure branch.", "workflow_node.try_catch.default_name": "Try to ...", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 85f1e578..a9be81f0 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -1,4 +1,4 @@ -{ +{ "provider.1panel": "1Panel", "provider.1panel.console": "1Panel - 面板自身", "provider.1panel.site": "1Panel - 网站", @@ -96,6 +96,7 @@ "provider.huaweicloud.dns": "华为云 - 云解析 DNS", "provider.huaweicloud.elb": "华为云 - 弹性负载均衡 ELB", "provider.huaweicloud.scm_upload": "华为云 - 上传到云证书管理服务 SCM", + "provider.huaweicloud.obs": "华为云 - 对象储存 OBS", "provider.huaweicloud.waf": "华为云 - Web 应用防火墙 WAF", "provider.jdcloud": "京东云", "provider.jdcloud.alb": "京东云 - 应用负载均衡 ALB", @@ -181,7 +182,6 @@ "provider.wecombot": "企业微信群机器人", "provider.westcn": "西部数码", "provider.zerossl": "ZeroSSL", - "provider.category.all": "全部", "provider.category.cdn": "CDN", "provider.category.storage": "文件存储", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 19bdbcaf..090f18a3 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -17,7 +17,6 @@ "workflow_node.start.form.trigger_cron.tooltip": "五段式表达式,使用 crontab 标准语法规则。
支持使用任意值(即 *)、值列表分隔符(即 ,)、值的范围(即 -)、步骤值(即 /)等四种表达式。", "workflow_node.start.form.trigger_cron.help": "预计最近 5 次运行时间(实际时区以服务器设置为准):", "workflow_node.start.form.trigger_cron.guide": "如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。也不要总是设置为每日零时,以免遭遇证书颁发机构的流量高峰。

参考链接:
1. Let’s Encrypt 速率限制
2. 为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?", - "workflow_node.apply.label": "申请签发证书", "workflow_node.apply.help": "从证书颁发机构申请签发 SSL 证书。", "workflow_node.apply.default_name": "申请", @@ -125,7 +124,6 @@ "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": "注意不要超过颁发的证书最大有效期,否则证书可能永远不会续期。", - "workflow_node.upload.label": "上传自有证书", "workflow_node.upload.help": "上传用户已有的本地 SSL 证书。", "workflow_node.upload.default_name": "上传", @@ -136,7 +134,6 @@ "workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----", "workflow_node.upload.form.private_key.label": "私钥文件(PEM 格式)", "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----", - "workflow_node.monitor.label": "监控网站证书", "workflow_node.monitor.help": "通过 HTTPS 协议获取指定网站的 SSL 证书。", "workflow_node.monitor.default_name": "监控", @@ -151,7 +148,6 @@ "workflow_node.monitor.form.domain.help": "提示:仅当主机地址为 IP 时需要输入。", "workflow_node.monitor.form.request_path.label": "请求路径(可选)", "workflow_node.monitor.form.request_path.placeholder": "请输入请求路径", - "workflow_node.deploy.label": "部署证书到 ...", "workflow_node.deploy.help": "调用服务提供商相关 API,将 SSL 证书部署到指定的目标。", "workflow_node.deploy.default_name": "部署", @@ -520,6 +516,15 @@ "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_obs_endpoint.label": "华为云 OBS 接口端点", + "workflow_node.deploy.form.huaweicloud_obs_endpoint.placeholder": "请输入华为云 OBS 接口端点(例如:obs.cn-north-4.myhuaweicloud.com)", + "workflow_node.deploy.form.huaweicloud_obs_endpoint.tooltip": "这是什么?请参阅 https://support.huaweicloud.com/productdesc-obs/obs_03_0152.html", + "workflow_node.deploy.form.huaweicloud_obs_bucket.label": "华为云 COS 存储桶名", + "workflow_node.deploy.form.huaweicloud_obs_bucket.placeholder": "请输入华为云 COS 存储桶名", + "workflow_node.deploy.form.huaweicloud_obs_bucket.tooltip": "这是什么?请参阅 https://support.huaweicloud.com/productdesc-obs/obs_03_0207.html", + "workflow_node.deploy.form.huaweicloud_obs_domain.label": "华为云 COS 自定义域名", + "workflow_node.deploy.form.huaweicloud_obs_domain.placeholder": "请输入华为云 COS 自定义域名", + "workflow_node.deploy.form.huaweicloud_obs_domain.tooltip": "这是什么?请参阅 https://support.huaweicloud.com/productdesc-obs/obs_03_0207.html", "workflow_node.deploy.form.huaweicloud_elb_region.label": "华为云 ELB 服务区域", "workflow_node.deploy.form.huaweicloud_elb_region.placeholder": "请输入华为云 ELB 服务区域(例如:cn-north-1)", "workflow_node.deploy.form.huaweicloud_elb_region.tooltip": "这是什么?请参阅 https://console.huaweicloud.com/apiexplorer/#/endpoint", @@ -1019,7 +1024,6 @@ "workflow_node.deploy.form.skip_on_last_succeeded.suffix": "此部署节点。", "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.help": "调用服务提供商相关 API,将消息通知推送到指定的目标。", "workflow_node.notify.default_name": "通知", @@ -1110,14 +1114,11 @@ "workflow_node.branch_block.form.expression.value.option.true.label": "真", "workflow_node.branch_block.form.expression.value.option.false.label": "假", "workflow_node.branch_block.form.expression.add_condition.button": "添加条件", - "workflow_node.try_catch.label": "执行结果分支", "workflow_node.try_catch.help": "尝试执行后续节点,当任一节点执行失败后,中断并进入执行失败分支。", "workflow_node.try_catch.default_name": "尝试执行…", - "workflow_node.catch_block.label": "执行失败分支", "workflow_node.catch_block.default_name": "若执行失败…", - "workflow_node.end.label": "结束", "workflow_node.end.help": "中止工作流运行并退出。通常是一个工作流的最后一个节点。", "workflow_node.end.default_name": "结束"