feat: support replacing old certificate on deployment to gcore cdn

This commit is contained in:
Fu Diwei 2025-05-12 23:19:13 +08:00 committed by RHQYZ
parent 9a0dd53cd7
commit f9633616e2
7 changed files with 101 additions and 34 deletions

View File

@ -563,6 +563,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
deployer, err := pGcoreCDN.NewDeployer(&pGcoreCDN.DeployerConfig{ deployer, err := pGcoreCDN.NewDeployer(&pGcoreCDN.DeployerConfig{
ApiToken: access.ApiToken, ApiToken: access.ApiToken,
ResourceId: maputil.GetInt64(options.ProviderExtendedConfig, "resourceId"), ResourceId: maputil.GetInt64(options.ProviderExtendedConfig, "resourceId"),
CertificateId: maputil.GetInt64(options.ProviderExtendedConfig, "certificateId"),
}) })
return deployer, err return deployer, err

View File

@ -63,6 +63,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
return nil, errors.New("config `domain` is required") return nil, errors.New("config `domain` is required")
} }
// 如果原证书 ID 为空,则新增证书;否则替换证书。
if d.config.CertificateId == "" { if d.config.CertificateId == "" {
// 新增证书 // 新增证书
// REF: https://portal.baishancloud.com/track/document/downloadPdf/1441 // REF: https://portal.baishancloud.com/track/document/downloadPdf/1441

View File

@ -8,8 +8,9 @@ import (
"strconv" "strconv"
"github.com/G-Core/gcorelabscdn-go/gcore" "github.com/G-Core/gcorelabscdn-go/gcore"
gprovider "github.com/G-Core/gcorelabscdn-go/gcore/provider" "github.com/G-Core/gcorelabscdn-go/gcore/provider"
"github.com/G-Core/gcorelabscdn-go/resources" "github.com/G-Core/gcorelabscdn-go/resources"
"github.com/G-Core/gcorelabscdn-go/sslcerts"
"github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader" "github.com/usual2970/certimate/internal/pkg/core/uploader"
@ -22,25 +23,33 @@ type DeployerConfig struct {
ApiToken string `json:"apiToken"` ApiToken string `json:"apiToken"`
// CDN 资源 ID。 // CDN 资源 ID。
ResourceId int64 `json:"resourceId"` ResourceId int64 `json:"resourceId"`
// 证书 ID。
// 选填。
CertificateId int64 `json:"certificateId,omitempty"`
} }
type DeployerProvider struct { type DeployerProvider struct {
config *DeployerConfig config *DeployerConfig
logger *slog.Logger logger *slog.Logger
sdkClient *resources.Service sdkClients *wSdkClients
sslUploader uploader.Uploader sslUploader uploader.Uploader
} }
var _ deployer.Deployer = (*DeployerProvider)(nil) var _ deployer.Deployer = (*DeployerProvider)(nil)
type wSdkClients struct {
Resources *resources.Service
SSLCerts *sslcerts.Service
}
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil { if config == nil {
panic("config is nil") panic("config is nil")
} }
client, err := createSdkClient(config.ApiToken) clients, err := createSdkClients(config.ApiToken)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err) return nil, fmt.Errorf("failed to create sdk clients: %w", err)
} }
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
@ -53,7 +62,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
return &DeployerProvider{ return &DeployerProvider{
config: config, config: config,
logger: slog.Default(), logger: slog.Default(),
sdkClient: client, sdkClients: clients,
sslUploader: uploader, sslUploader: uploader,
}, nil }, nil
} }
@ -73,6 +82,9 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
return nil, errors.New("config `resourceId` is required") return nil, errors.New("config `resourceId` is required")
} }
// 如果原证书 ID 为空,则创建证书;否则更新证书。
var cloudCertId int64
if d.config.CertificateId == 0 {
// 上传证书到 CDN // 上传证书到 CDN
upres, err := d.sslUploader.Upload(ctx, certPEM, privkeyPEM) upres, err := d.sslUploader.Upload(ctx, certPEM, privkeyPEM)
if err != nil { if err != nil {
@ -81,9 +93,36 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
} }
cloudCertId, _ = strconv.ParseInt(upres.CertId, 10, 64)
} else {
// 获取证书
// REF: https://api.gcore.com/docs/cdn#tag/SSL-certificates/paths/~1cdn~1sslData~1%7Bssl_id%7D/get
getCertificateDetailResp, err := d.sdkClients.SSLCerts.Get(context.TODO(), d.config.CertificateId)
d.logger.Debug("sdk request 'sslcerts.Get'", slog.Any("sslId", d.config.CertificateId), slog.Any("response", getCertificateDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'sslcerts.Get': %w", err)
}
// 更新证书
// REF: https://api.gcore.com/docs/cdn#tag/SSL-certificates/paths/~1cdn~1sslData~1%7Bssl_id%7D/get
changeCertificateReq := &sslcerts.UpdateRequest{
Name: getCertificateDetailResp.Name,
Cert: certPEM,
PrivateKey: privkeyPEM,
ValidateRootCA: false,
}
changeCertificateResp, err := d.sdkClients.SSLCerts.Update(context.TODO(), getCertificateDetailResp.ID, changeCertificateReq)
d.logger.Debug("sdk request 'sslcerts.Create'", slog.Any("request", changeCertificateReq), slog.Any("response", changeCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'sslcerts.Update': %w", err)
}
cloudCertId = changeCertificateResp.ID
}
// 获取 CDN 资源详情 // 获取 CDN 资源详情
// REF: https://api.gcore.com/docs/cdn#tag/CDN-resources/paths/~1cdn~1resources~1%7Bresource_id%7D/get // REF: https://api.gcore.com/docs/cdn#tag/CDN-resources/paths/~1cdn~1resources~1%7Bresource_id%7D/get
getResourceResp, err := d.sdkClient.Get(context.TODO(), d.config.ResourceId) getResourceResp, err := d.sdkClients.Resources.Get(context.TODO(), d.config.ResourceId)
d.logger.Debug("sdk request 'resources.Get'", slog.Any("resourceId", d.config.ResourceId), slog.Any("response", getResourceResp)) d.logger.Debug("sdk request 'resources.Get'", slog.Any("resourceId", d.config.ResourceId), slog.Any("response", getResourceResp))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'resources.Get': %w", err) return nil, fmt.Errorf("failed to execute sdk request 'resources.Get': %w", err)
@ -91,7 +130,6 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
// 更新 CDN 资源详情 // 更新 CDN 资源详情
// REF: https://api.gcore.com/docs/cdn#tag/CDN-resources/operation/change_cdn_resource // REF: https://api.gcore.com/docs/cdn#tag/CDN-resources/operation/change_cdn_resource
updateResourceCertId, _ := strconv.ParseInt(upres.CertId, 10, 64)
updateResourceReq := &resources.UpdateRequest{ updateResourceReq := &resources.UpdateRequest{
Description: getResourceResp.Description, Description: getResourceResp.Description,
Active: getResourceResp.Active, Active: getResourceResp.Active,
@ -99,7 +137,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
OriginProtocol: getResourceResp.OriginProtocol, OriginProtocol: getResourceResp.OriginProtocol,
SecondaryHostnames: getResourceResp.SecondaryHostnames, SecondaryHostnames: getResourceResp.SecondaryHostnames,
SSlEnabled: true, SSlEnabled: true,
SSLData: int(updateResourceCertId), SSLData: int(cloudCertId),
ProxySSLEnabled: getResourceResp.ProxySSLEnabled, ProxySSLEnabled: getResourceResp.ProxySSLEnabled,
Options: &gcore.Options{}, Options: &gcore.Options{},
} }
@ -109,7 +147,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
if getResourceResp.ProxySSLData != 0 { if getResourceResp.ProxySSLData != 0 {
updateResourceReq.ProxySSLData = &getResourceResp.ProxySSLData updateResourceReq.ProxySSLData = &getResourceResp.ProxySSLData
} }
updateResourceResp, err := d.sdkClient.Update(context.TODO(), d.config.ResourceId, updateResourceReq) updateResourceResp, err := d.sdkClients.Resources.Update(context.TODO(), d.config.ResourceId, updateResourceReq)
d.logger.Debug("sdk request 'resources.Update'", slog.Int64("resourceId", d.config.ResourceId), slog.Any("request", updateResourceReq), slog.Any("response", updateResourceResp)) d.logger.Debug("sdk request 'resources.Update'", slog.Int64("resourceId", d.config.ResourceId), slog.Any("request", updateResourceReq), slog.Any("response", updateResourceResp))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'resources.Update': %w", err) return nil, fmt.Errorf("failed to execute sdk request 'resources.Update': %w", err)
@ -118,15 +156,19 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
return &deployer.DeployResult{}, nil return &deployer.DeployResult{}, nil
} }
func createSdkClient(apiToken string) (*resources.Service, error) { func createSdkClients(apiToken string) (*wSdkClients, error) {
if apiToken == "" { if apiToken == "" {
return nil, errors.New("invalid gcore api token") return nil, errors.New("invalid gcore api token")
} }
requester := gprovider.NewClient( requester := provider.NewClient(
gcoresdk.BASE_URL, gcoresdk.BASE_URL,
gprovider.WithSigner(gcoresdk.NewAuthRequestSigner(apiToken)), provider.WithSigner(gcoresdk.NewAuthRequestSigner(apiToken)),
) )
service := resources.NewService(requester) resourcesSrv := resources.NewService(requester)
return service, nil sslCertsSrv := sslcerts.NewService(requester)
return &wSdkClients{
Resources: resourcesSrv,
SSLCerts: sslCertsSrv,
}, nil
} }

View File

@ -7,8 +7,8 @@ import (
"log/slog" "log/slog"
"time" "time"
gprovider "github.com/G-Core/gcorelabscdn-go/gcore/provider" "github.com/G-Core/gcorelabscdn-go/gcore/provider"
gsslcerts "github.com/G-Core/gcorelabscdn-go/sslcerts" "github.com/G-Core/gcorelabscdn-go/sslcerts"
"github.com/usual2970/certimate/internal/pkg/core/uploader" "github.com/usual2970/certimate/internal/pkg/core/uploader"
gcoresdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/gcore/common" gcoresdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/gcore/common"
@ -22,7 +22,7 @@ type UploaderConfig struct {
type UploaderProvider struct { type UploaderProvider struct {
config *UploaderConfig config *UploaderConfig
logger *slog.Logger logger *slog.Logger
sdkClient *gsslcerts.Service sdkClient *sslcerts.Service
} }
var _ uploader.Uploader = (*UploaderProvider)(nil) var _ uploader.Uploader = (*UploaderProvider)(nil)
@ -59,8 +59,8 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
certName = fmt.Sprintf("certimate_%d", time.Now().UnixMilli()) certName = fmt.Sprintf("certimate_%d", time.Now().UnixMilli())
// 新增证书 // 新增证书
// REF: https://api.gcore.com/docs/cdn#tag/CA-certificates/operation/ca_certificates-add // REF: https://api.gcore.com/docs/cdn#tag/SSL-certificates/operation/add_ssl_certificates
createCertificateReq := &gsslcerts.CreateRequest{ createCertificateReq := &sslcerts.CreateRequest{
Name: certName, Name: certName,
Cert: certPEM, Cert: certPEM,
PrivateKey: privkeyPEM, PrivateKey: privkeyPEM,
@ -81,15 +81,15 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
}, nil }, nil
} }
func createSdkClient(apiToken string) (*gsslcerts.Service, error) { func createSdkClient(apiToken string) (*sslcerts.Service, error) {
if apiToken == "" { if apiToken == "" {
return nil, errors.New("invalid gcore api token") return nil, errors.New("invalid gcore api token")
} }
requester := gprovider.NewClient( requester := provider.NewClient(
gcoresdk.BASE_URL, gcoresdk.BASE_URL,
gprovider.WithSigner(gcoresdk.NewAuthRequestSigner(apiToken)), provider.WithSigner(gcoresdk.NewAuthRequestSigner(apiToken)),
) )
service := gsslcerts.NewService(requester) service := sslcerts.NewService(requester)
return service, nil return service, nil
} }

View File

@ -5,6 +5,7 @@ import { z } from "zod";
type DeployNodeConfigFormGcoreCDNConfigFieldValues = Nullish<{ type DeployNodeConfigFormGcoreCDNConfigFieldValues = Nullish<{
resourceId: string | number; resourceId: string | number;
certificateId?: string | number;
}>; }>;
export type DeployNodeConfigFormGcoreCDNConfigProps = { export type DeployNodeConfigFormGcoreCDNConfigProps = {
@ -28,6 +29,13 @@ const DeployNodeConfigFormGcoreCDNConfig = ({ form: formInst, formName, disabled
resourceId: z.union([z.string(), z.number()]).refine((v) => { resourceId: z.union([z.string(), z.number()]).refine((v) => {
return /^\d+$/.test(v + "") && +v > 0; return /^\d+$/.test(v + "") && +v > 0;
}, t("workflow_node.deploy.form.gcore_cdn_resource_id.placeholder")), }, t("workflow_node.deploy.form.gcore_cdn_resource_id.placeholder")),
certificateId: z
.union([z.string(), z.number().int()])
.nullish()
.refine((v) => {
if (!v) return true;
return /^\d+$/.test(v + "") && +v > 0;
}, t("workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder")),
}); });
const formRule = createSchemaFieldRule(formSchema); const formRule = createSchemaFieldRule(formSchema);
@ -52,6 +60,15 @@ const DeployNodeConfigFormGcoreCDNConfig = ({ form: formInst, formName, disabled
> >
<Input type="number" placeholder={t("workflow_node.deploy.form.gcore_cdn_resource_id.placeholder")} /> <Input type="number" placeholder={t("workflow_node.deploy.form.gcore_cdn_resource_id.placeholder")} />
</Form.Item> </Form.Item>
<Form.Item
name="certificateId"
label={t("workflow_node.deploy.form.gcore_cdn_certificate_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.gcore_cdn_certificate_id.tooltip") }}></span>}
>
<Input type="number" placeholder={t("workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder")} />
</Form.Item>
</Form> </Form>
); );
}; };

View File

@ -356,6 +356,9 @@
"workflow_node.deploy.form.gcore_cdn_resource_id.label": "Gcore CDN resource ID", "workflow_node.deploy.form.gcore_cdn_resource_id.label": "Gcore CDN resource ID",
"workflow_node.deploy.form.gcore_cdn_resource_id.placeholder": "Please enter Gcore CDN resource ID", "workflow_node.deploy.form.gcore_cdn_resource_id.placeholder": "Please enter Gcore CDN resource ID",
"workflow_node.deploy.form.gcore_cdn_resource_id.tooltip": "For more information, see <a href=\"https://cdn.gcore.com/resources/list\" target=\"_blank\">https://cdn.gcore.com/resources/list</a>", "workflow_node.deploy.form.gcore_cdn_resource_id.tooltip": "For more information, see <a href=\"https://cdn.gcore.com/resources/list\" target=\"_blank\">https://cdn.gcore.com/resources/list</a>",
"workflow_node.deploy.form.gcore_cdn_certificate_id.label": "Gcore CDN certificate ID (Optional)",
"workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder": "Please enter Gcore CDN certificate ID",
"workflow_node.deploy.form.gcore_cdn_certificate_id.tooltip": "For more information, see <a href=\"https://cdn.gcore.com/ssl\" target=\"_blank\">https://cdn.gcore.com/ssl</a>",
"workflow_node.deploy.form.goedge_resource_type.label": "Resource type", "workflow_node.deploy.form.goedge_resource_type.label": "Resource type",
"workflow_node.deploy.form.goedge_resource_type.placeholder": "Please select resource type", "workflow_node.deploy.form.goedge_resource_type.placeholder": "Please select resource type",
"workflow_node.deploy.form.goedge_resource_type.option.certificate.label": "Certificate", "workflow_node.deploy.form.goedge_resource_type.option.certificate.label": "Certificate",
@ -441,7 +444,7 @@
"workflow_node.deploy.form.local_format.label": "File format", "workflow_node.deploy.form.local_format.label": "File format",
"workflow_node.deploy.form.local_format.placeholder": "Please select file format", "workflow_node.deploy.form.local_format.placeholder": "Please select file format",
"workflow_node.deploy.form.local_format.option.pem.label": "PEM (*.pem, *.crt, *.key)", "workflow_node.deploy.form.local_format.option.pem.label": "PEM (*.pem, *.crt, *.key)",
"workflow_node.deploy.form.local_format.option.pfx.label": "PFX (*.pfx)", "workflow_node.deploy.form.local_format.option.pfx.label": "PFX (*.pfx, *.p12)",
"workflow_node.deploy.form.local_format.option.jks.label": "JKS (*.jks)", "workflow_node.deploy.form.local_format.option.jks.label": "JKS (*.jks)",
"workflow_node.deploy.form.local_cert_path.label": "Certificate file saving path", "workflow_node.deploy.form.local_cert_path.label": "Certificate file saving path",
"workflow_node.deploy.form.local_cert_path.placeholder": "Please enter saving path for certificate file", "workflow_node.deploy.form.local_cert_path.placeholder": "Please enter saving path for certificate file",
@ -506,7 +509,7 @@
"workflow_node.deploy.form.ssh_format.label": "File format", "workflow_node.deploy.form.ssh_format.label": "File format",
"workflow_node.deploy.form.ssh_format.placeholder": "Please select file format", "workflow_node.deploy.form.ssh_format.placeholder": "Please select file format",
"workflow_node.deploy.form.ssh_format.option.pem.label": "PEM (*.pem, *.crt, *.key)", "workflow_node.deploy.form.ssh_format.option.pem.label": "PEM (*.pem, *.crt, *.key)",
"workflow_node.deploy.form.ssh_format.option.pfx.label": "PFX (*.pfx)", "workflow_node.deploy.form.ssh_format.option.pfx.label": "PFX (*.pfx, *.p12)",
"workflow_node.deploy.form.ssh_format.option.jks.label": "JKS (*.jks)", "workflow_node.deploy.form.ssh_format.option.jks.label": "JKS (*.jks)",
"workflow_node.deploy.form.ssh_cert_path.label": "Certificate file uploading path", "workflow_node.deploy.form.ssh_cert_path.label": "Certificate file uploading path",
"workflow_node.deploy.form.ssh_cert_path.placeholder": "Please enter uploading path for certificate file", "workflow_node.deploy.form.ssh_cert_path.placeholder": "Please enter uploading path for certificate file",

View File

@ -355,6 +355,9 @@
"workflow_node.deploy.form.gcore_cdn_resource_id.label": "Gcore CDN 资源 ID", "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.placeholder": "请输入 Gcore CDN 资源 ID",
"workflow_node.deploy.form.gcore_cdn_resource_id.tooltip": "这是什么?请参阅 <a href=\"https://cdn.gcore.com/resources/list\" target=\"_blank\">https://cdn.gcore.com/resources/list</a>", "workflow_node.deploy.form.gcore_cdn_resource_id.tooltip": "这是什么?请参阅 <a href=\"https://cdn.gcore.com/resources/list\" target=\"_blank\">https://cdn.gcore.com/resources/list</a>",
"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": "这是什么?请参阅 <a href=\"https://cdn.gcore.com/ssl\" target=\"_blank\">https://cdn.gcore.com/ssl</a><br><br>不填写时,将上传新证书;否则,将替换原证书。",
"workflow_node.deploy.form.goedge_resource_type.label": "证书替换方式", "workflow_node.deploy.form.goedge_resource_type.label": "证书替换方式",
"workflow_node.deploy.form.goedge_resource_type.placeholder": "请选择证书替换方式", "workflow_node.deploy.form.goedge_resource_type.placeholder": "请选择证书替换方式",
"workflow_node.deploy.form.goedge_resource_type.option.certificate.label": "替换指定证书", "workflow_node.deploy.form.goedge_resource_type.option.certificate.label": "替换指定证书",
@ -440,7 +443,7 @@
"workflow_node.deploy.form.local_format.label": "文件格式", "workflow_node.deploy.form.local_format.label": "文件格式",
"workflow_node.deploy.form.local_format.placeholder": "请选择文件格式", "workflow_node.deploy.form.local_format.placeholder": "请选择文件格式",
"workflow_node.deploy.form.local_format.option.pem.label": "PEM 格式(*.pem, *.crt, *.key", "workflow_node.deploy.form.local_format.option.pem.label": "PEM 格式(*.pem, *.crt, *.key",
"workflow_node.deploy.form.local_format.option.pfx.label": "PFX 格式(*.pfx", "workflow_node.deploy.form.local_format.option.pfx.label": "PFX 格式(*.pfx, *.p12",
"workflow_node.deploy.form.local_format.option.jks.label": "JKS 格式(*.jks", "workflow_node.deploy.form.local_format.option.jks.label": "JKS 格式(*.jks",
"workflow_node.deploy.form.local_cert_path.label": "证书文件保存路径", "workflow_node.deploy.form.local_cert_path.label": "证书文件保存路径",
"workflow_node.deploy.form.local_cert_path.placeholder": "请输入证书文件保存路径", "workflow_node.deploy.form.local_cert_path.placeholder": "请输入证书文件保存路径",
@ -505,7 +508,7 @@
"workflow_node.deploy.form.ssh_format.label": "文件格式", "workflow_node.deploy.form.ssh_format.label": "文件格式",
"workflow_node.deploy.form.ssh_format.placeholder": "请选择文件格式", "workflow_node.deploy.form.ssh_format.placeholder": "请选择文件格式",
"workflow_node.deploy.form.ssh_format.option.pem.label": "PEM 格式(*.pem, *.crt, *.key", "workflow_node.deploy.form.ssh_format.option.pem.label": "PEM 格式(*.pem, *.crt, *.key",
"workflow_node.deploy.form.ssh_format.option.pfx.label": "PFX 格式(*.pfx", "workflow_node.deploy.form.ssh_format.option.pfx.label": "PFX 格式(*.pfx, *.p12",
"workflow_node.deploy.form.ssh_format.option.jks.label": "JKS 格式(*.jks", "workflow_node.deploy.form.ssh_format.option.jks.label": "JKS 格式(*.jks",
"workflow_node.deploy.form.ssh_cert_path.label": "证书文件上传路径", "workflow_node.deploy.form.ssh_cert_path.label": "证书文件上传路径",
"workflow_node.deploy.form.ssh_cert_path.placeholder": "请输入证书文件上传路径", "workflow_node.deploy.form.ssh_cert_path.placeholder": "请输入证书文件上传路径",