feat(provider): new deployment provider: s3

This commit is contained in:
Fu Diwei 2026-01-16 21:03:43 +08:00 committed by RHQYZ
parent edb858cfec
commit c0bcf6d0fd
21 changed files with 703 additions and 85 deletions

View File

@ -59,7 +59,7 @@ func (s *CertificateService) DownloadArchivedFile(ctx context.Context, req *dtos
var bytes []byte
switch strings.ToUpper(req.CertificateFormat) {
case "", "PEM":
case "", string(domain.CertificateFormatTypePEM):
{
serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certificate.Certificate)
if err != nil {
@ -114,7 +114,7 @@ func (s *CertificateService) DownloadArchivedFile(ctx context.Context, req *dtos
bytes = buf.Bytes()
}
case "PFX":
case string(domain.CertificateFormatTypePFX):
{
const pfxPassword = "certimate"
@ -151,7 +151,7 @@ func (s *CertificateService) DownloadArchivedFile(ctx context.Context, req *dtos
bytes = buf.Bytes()
}
case "JKS":
case string(domain.CertificateFormatTypeJKS):
{
const jksPassword = "certimate"

View File

@ -0,0 +1,40 @@
package deployers
import (
"fmt"
"github.com/certimate-go/certimate/internal/domain"
"github.com/certimate-go/certimate/pkg/core/deployer"
"github.com/certimate-go/certimate/pkg/core/deployer/providers/s3"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
Registries.MustRegister(domain.DeploymentProviderTypeS3, func(options *ProviderFactoryOptions) (deployer.Provider, error) {
credentials := domain.AccessConfigForS3{}
if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
provider, err := s3.NewDeployer(&s3.DeployerConfig{
Endpoint: credentials.Endpoint,
AccessKey: credentials.AccessKey,
SecretKey: credentials.SecretKey,
SignatureVersion: credentials.SignatureVersion,
UsePathStyle: credentials.UsePathStyle,
AllowInsecureConnections: credentials.AllowInsecureConnections,
Region: xmaps.GetString(options.ProviderExtendedConfig, "region"),
Bucket: xmaps.GetString(options.ProviderExtendedConfig, "bucket"),
OutputFormat: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "format", s3.OUTPUT_FORMAT_PEM),
OutputCertObjectKey: xmaps.GetString(options.ProviderExtendedConfig, "certObjectKey"),
OutputServerCertObjectKey: xmaps.GetString(options.ProviderExtendedConfig, "certObjectKeyForServerOnly"),
OutputIntermediaCertObjectKey: xmaps.GetString(options.ProviderExtendedConfig, "certObjectKeyForIntermediaOnly"),
OutputKeyObjectKey: xmaps.GetString(options.ProviderExtendedConfig, "keyObjectKey"),
PfxPassword: xmaps.GetString(options.ProviderExtendedConfig, "pfxPassword"),
JksAlias: xmaps.GetString(options.ProviderExtendedConfig, "jksAlias"),
JksKeypass: xmaps.GetString(options.ProviderExtendedConfig, "jksKeypass"),
JksStorepass: xmaps.GetString(options.ProviderExtendedConfig, "jksStorepass"),
})
return provider, err
})
}

View File

@ -112,3 +112,11 @@ func (t CertificateKeyAlgorithmType) KeyType() (certcrypto.KeyType, error) {
return certcrypto.RSA2048, fmt.Errorf("unsupported key algorithm type: '%s'", t)
}
type CertificateFormatType string
const (
CertificateFormatTypePEM CertificateFormatType = "PEM"
CertificateFormatTypePFX CertificateFormatType = "PFX"
CertificateFormatTypeJKS CertificateFormatType = "JKS"
)

View File

@ -329,6 +329,7 @@ const (
DeploymentProviderTypeRainYunRCDN = DeploymentProviderType(AccessProviderTypeRainYun + "-rcdn")
DeploymentProviderTypeRatPanel = DeploymentProviderType(AccessProviderTypeRatPanel)
DeploymentProviderTypeRatPanelConsole = DeploymentProviderType(AccessProviderTypeRatPanel + "-console")
DeploymentProviderTypeS3 = DeploymentProviderType(AccessProviderTypeS3)
DeploymentProviderTypeSafeLine = DeploymentProviderType(AccessProviderTypeSafeLine)
DeploymentProviderTypeSSH = DeploymentProviderType(AccessProviderTypeSSH)
DeploymentProviderTypeSynologyDSM = DeploymentProviderType(AccessProviderTypeSynologyDSM)

View File

@ -1,6 +1,7 @@
package s3
import (
"bytes"
"context"
"errors"
"fmt"
@ -87,11 +88,11 @@ func NewClient(config *Config) (*Client, error) {
}, nil
}
func (c *Client) PutObject(ctx context.Context, bucket, key string, reader io.Reader) error {
func (c *Client) PutObject(ctx context.Context, bucket, key string, reader io.Reader, size int64) error {
putOpts := minio.PutObjectOptions{
DisableMultipart: true,
}
_, err := c.client.PutObject(ctx, bucket, key, reader, 1024*1024*16, putOpts)
_, err := c.client.PutObject(ctx, bucket, key, reader, size, putOpts)
if err != nil {
return err
}
@ -99,6 +100,16 @@ func (c *Client) PutObject(ctx context.Context, bucket, key string, reader io.Re
return nil
}
func (c *Client) PutObjectString(ctx context.Context, bucket, key string, data string) error {
reader := strings.NewReader(data)
return c.PutObject(ctx, bucket, key, reader, reader.Size())
}
func (c *Client) PutObjectBytes(ctx context.Context, bucket, key string, data []byte) error {
reader := bytes.NewReader(data)
return c.PutObject(ctx, bucket, key, reader, reader.Size())
}
func (c *Client) RemoveObject(ctx context.Context, bucket, key string) error {
removeOpts := minio.RemoveObjectOptions{}
err := c.client.RemoveObject(ctx, bucket, key, removeOpts)

View File

@ -62,8 +62,7 @@ type provider struct {
func (p *provider) Present(domain, token, keyAuth string) error {
objectKey := strings.Trim(http01.ChallengePath(token), "/")
reader := strings.NewReader(keyAuth)
if err := p.client.PutObject(context.Background(), p.bucket, objectKey, reader); err != nil {
if err := p.client.PutObjectString(context.Background(), p.bucket, objectKey, keyAuth); err != nil {
return fmt.Errorf("s3: failed to upload token to s3: %w", err)
}

View File

@ -1,9 +1,7 @@
package local
const (
OUTPUT_FORMAT_PEM = "PEM"
OUTPUT_FORMAT_PFX = "PFX"
OUTPUT_FORMAT_JKS = "JKS"
import (
"github.com/certimate-go/certimate/internal/domain"
)
const (
@ -11,3 +9,9 @@ const (
SHELL_ENV_CMD = "cmd"
SHELL_ENV_POWERSHELL = "powershell"
)
const (
OUTPUT_FORMAT_PEM = string(domain.CertificateFormatTypePEM)
OUTPUT_FORMAT_PFX = string(domain.CertificateFormatTypePFX)
OUTPUT_FORMAT_JKS = string(domain.CertificateFormatTypeJKS)
)

View File

@ -104,53 +104,59 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*dep
// 写入证书和私钥文件
switch d.config.OutputFormat {
case OUTPUT_FORMAT_PEM:
if err := xfile.WriteString(d.config.OutputCertPath, certPEM); err != nil {
return nil, fmt.Errorf("failed to save certificate file: %w", err)
}
d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath))
if d.config.OutputServerCertPath != "" {
if err := xfile.WriteString(d.config.OutputServerCertPath, serverCertPEM); err != nil {
return nil, fmt.Errorf("failed to save server certificate file: %w", err)
{
if err := xfile.WriteString(d.config.OutputCertPath, certPEM); err != nil {
return nil, fmt.Errorf("failed to save certificate file: %w", err)
}
d.logger.Info("ssl server certificate file saved", slog.String("path", d.config.OutputServerCertPath))
}
d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath))
if d.config.OutputIntermediaCertPath != "" {
if err := xfile.WriteString(d.config.OutputIntermediaCertPath, intermediaCertPEM); err != nil {
return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err)
if d.config.OutputServerCertPath != "" {
if err := xfile.WriteString(d.config.OutputServerCertPath, serverCertPEM); err != nil {
return nil, fmt.Errorf("failed to save server certificate file: %w", err)
}
d.logger.Info("ssl server certificate file saved", slog.String("path", d.config.OutputServerCertPath))
}
d.logger.Info("ssl intermedia certificate file saved", slog.String("path", d.config.OutputIntermediaCertPath))
}
if err := xfile.WriteString(d.config.OutputKeyPath, privkeyPEM); err != nil {
return nil, fmt.Errorf("failed to save private key file: %w", err)
if d.config.OutputIntermediaCertPath != "" {
if err := xfile.WriteString(d.config.OutputIntermediaCertPath, intermediaCertPEM); err != nil {
return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err)
}
d.logger.Info("ssl intermedia certificate file saved", slog.String("path", d.config.OutputIntermediaCertPath))
}
if err := xfile.WriteString(d.config.OutputKeyPath, privkeyPEM); err != nil {
return nil, fmt.Errorf("failed to save private key file: %w", err)
}
d.logger.Info("ssl private key file saved", slog.String("path", d.config.OutputKeyPath))
}
d.logger.Info("ssl private key file saved", slog.String("path", d.config.OutputKeyPath))
case OUTPUT_FORMAT_PFX:
pfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword)
if err != nil {
return nil, fmt.Errorf("failed to transform certificate to PFX: %w", err)
}
d.logger.Info("ssl certificate transformed to pfx")
{
pfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword)
if err != nil {
return nil, fmt.Errorf("failed to transform certificate to PFX: %w", err)
}
d.logger.Info("ssl certificate transformed to pfx")
if err := xfile.Write(d.config.OutputCertPath, pfxData); err != nil {
return nil, fmt.Errorf("failed to save certificate file: %w", err)
if err := xfile.Write(d.config.OutputCertPath, pfxData); err != nil {
return nil, fmt.Errorf("failed to save certificate file: %w", err)
}
d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath))
}
d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath))
case OUTPUT_FORMAT_JKS:
jksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)
if err != nil {
return nil, fmt.Errorf("failed to transform certificate to JKS: %w", err)
}
d.logger.Info("ssl certificate transformed to jks")
{
jksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)
if err != nil {
return nil, fmt.Errorf("failed to transform certificate to JKS: %w", err)
}
d.logger.Info("ssl certificate transformed to jks")
if err := xfile.Write(d.config.OutputCertPath, jksData); err != nil {
return nil, fmt.Errorf("failed to save certificate file: %w", err)
if err := xfile.Write(d.config.OutputCertPath, jksData); err != nil {
return nil, fmt.Errorf("failed to save certificate file: %w", err)
}
d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath))
}
d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath))
default:
return nil, fmt.Errorf("unsupported output format '%s'", d.config.OutputFormat)

View File

@ -0,0 +1,11 @@
package s3
import (
"github.com/certimate-go/certimate/internal/domain"
)
const (
OUTPUT_FORMAT_PEM = string(domain.CertificateFormatTypePEM)
OUTPUT_FORMAT_PFX = string(domain.CertificateFormatTypePFX)
OUTPUT_FORMAT_JKS = string(domain.CertificateFormatTypeJKS)
)

View File

@ -0,0 +1,169 @@
package s3
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/certimate-go/certimate/internal/tools/s3"
"github.com/certimate-go/certimate/pkg/core/deployer"
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
)
type DeployerConfig struct {
// S3 Endpoint。
Endpoint string `json:"endpoint"`
// S3 AccessKey。
AccessKey string `json:"accessKey"`
// S3 SecretKey。
SecretKey string `json:"secretKey"`
// S3 签名版本。
// 可取值 "v2"、"v4"。
// 零值时默认值 "v4"。
SignatureVersion string `json:"signatureVersion,omitempty"`
// 是否使用路径风格。
UsePathStyle bool `json:"usePathStyle,omitempty"`
// 存储区域。
Region string `json:"region"`
// 存储桶名。
Bucket string `json:"bucket"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 输出证书格式。
OutputFormat string `json:"outputFormat,omitempty"`
// 输出证书文件路径。
OutputCertObjectKey string `json:"outputCertObjectKey,omitempty"`
// 输出服务器证书文件路径。
// 选填。
OutputServerCertObjectKey string `json:"outputServerCertObjectKey,omitempty"`
// 输出中间证书文件路径。
// 选填。
OutputIntermediaCertObjectKey string `json:"outputIntermediaCertObjectKey,omitempty"`
// 输出私钥文件路径。
OutputKeyObjectKey string `json:"outputKeyObjectKey,omitempty"`
// PFX 导出密码。
// 证书格式为 PFX 时必填。
PfxPassword string `json:"pfxPassword,omitempty"`
// JKS 别名。
// 证书格式为 JKS 时必填。
JksAlias string `json:"jksAlias,omitempty"`
// JKS 密钥密码。
// 证书格式为 JKS 时必填。
JksKeypass string `json:"jksKeypass,omitempty"`
// JKS 存储密码。
// 证书格式为 JKS 时必填。
JksStorepass string `json:"jksStorepass,omitempty"`
}
type Deployer struct {
config *DeployerConfig
logger *slog.Logger
s3Client *s3.Client
}
var _ deployer.Provider = (*Deployer)(nil)
func NewDeployer(config *DeployerConfig) (*Deployer, error) {
if config == nil {
return nil, errors.New("the configuration of the deployer provider is nil")
}
client, err := s3.NewClient(&s3.Config{
Endpoint: config.Endpoint,
AccessKey: config.AccessKey,
SecretKey: config.SecretKey,
SignatureVersion: config.SignatureVersion,
UsePathStyle: config.UsePathStyle,
Region: config.Region,
SkipTlsVerify: config.AllowInsecureConnections,
})
if err != nil {
return nil, fmt.Errorf("s3: failed to create s3 client: %w", err)
}
return &Deployer{
config: config,
logger: slog.Default(),
s3Client: client,
}, nil
}
func (d *Deployer) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) {
// 提取服务器证书和中间证书
serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 写入证书和私钥文件
switch d.config.OutputFormat {
case OUTPUT_FORMAT_PEM:
{
if err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputCertObjectKey, certPEM); err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
}
d.logger.Info("ssl certificate file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputCertObjectKey))
if d.config.OutputServerCertObjectKey != "" {
if err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputServerCertObjectKey, serverCertPEM); err != nil {
return nil, fmt.Errorf("failed to upload server certificate file: %w", err)
}
d.logger.Info("ssl server certificate file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputServerCertObjectKey))
}
if d.config.OutputIntermediaCertObjectKey != "" {
if err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputIntermediaCertObjectKey, intermediaCertPEM); err != nil {
return nil, fmt.Errorf("failed to upload intermedia certificate file: %w", err)
}
d.logger.Info("ssl intermedia certificate file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputIntermediaCertObjectKey))
}
if err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputKeyObjectKey, privkeyPEM); err != nil {
return nil, fmt.Errorf("failed to upload private key file: %w", err)
}
d.logger.Info("ssl private key file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputKeyObjectKey))
}
case OUTPUT_FORMAT_PFX:
{
pfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword)
if err != nil {
return nil, fmt.Errorf("failed to transform certificate to PFX: %w", err)
}
d.logger.Info("ssl certificate transformed to pfx")
if err := d.s3Client.PutObjectBytes(ctx, d.config.Bucket, d.config.OutputCertObjectKey, pfxData); err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
}
d.logger.Info("ssl certificate file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputCertObjectKey))
}
case OUTPUT_FORMAT_JKS:
{
jksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)
if err != nil {
return nil, fmt.Errorf("failed to transform certificate to JKS: %w", err)
}
d.logger.Info("ssl certificate transformed to jks")
if err := d.s3Client.PutObjectBytes(ctx, d.config.Bucket, d.config.OutputCertObjectKey, jksData); err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
}
d.logger.Info("ssl certificate file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputCertObjectKey))
}
default:
return nil, fmt.Errorf("unsupported output format '%s'", d.config.OutputFormat)
}
return &deployer.DeployResult{}, nil
}

View File

@ -1,5 +1,9 @@
package ssh
import (
"github.com/certimate-go/certimate/internal/domain"
)
const (
AUTH_METHOD_NONE = "none"
AUTH_METHOD_PASSWORD = "password"
@ -7,7 +11,8 @@ const (
)
const (
OUTPUT_FORMAT_PEM = "PEM"
OUTPUT_FORMAT_PFX = "PFX"
OUTPUT_FORMAT_JKS = "JKS"
OUTPUT_FORMAT_PEM = string(domain.CertificateFormatTypePEM)
OUTPUT_FORMAT_PFX = string(domain.CertificateFormatTypePFX)
OUTPUT_FORMAT_JKS = string(domain.CertificateFormatTypeJKS)
)

View File

@ -202,53 +202,59 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*dep
// 上传证书和私钥文件
switch d.config.OutputFormat {
case OUTPUT_FORMAT_PEM:
if err := xssh.WriteRemoteString(client, d.config.OutputKeyPath, privkeyPEM, d.config.UseSCP); err != nil {
return nil, fmt.Errorf("failed to upload private key file: %w", err)
}
d.logger.Info("ssl private key file uploaded", slog.String("path", d.config.OutputKeyPath))
if err := xssh.WriteRemoteString(client, d.config.OutputCertPath, certPEM, d.config.UseSCP); err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
}
d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath))
if d.config.OutputServerCertPath != "" {
if err := xssh.WriteRemoteString(client, d.config.OutputServerCertPath, serverCertPEM, d.config.UseSCP); err != nil {
return nil, fmt.Errorf("failed to save server certificate file: %w", err)
{
if err := xssh.WriteRemoteString(client, d.config.OutputCertPath, certPEM, d.config.UseSCP); err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
}
d.logger.Info("ssl server certificate file uploaded", slog.String("path", d.config.OutputServerCertPath))
}
d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath))
if d.config.OutputIntermediaCertPath != "" {
if err := xssh.WriteRemoteString(client, d.config.OutputIntermediaCertPath, intermediaCertPEM, d.config.UseSCP); err != nil {
return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err)
if d.config.OutputServerCertPath != "" {
if err := xssh.WriteRemoteString(client, d.config.OutputServerCertPath, serverCertPEM, d.config.UseSCP); err != nil {
return nil, fmt.Errorf("failed to save server certificate file: %w", err)
}
d.logger.Info("ssl server certificate file uploaded", slog.String("path", d.config.OutputServerCertPath))
}
d.logger.Info("ssl intermedia certificate file uploaded", slog.String("path", d.config.OutputIntermediaCertPath))
if d.config.OutputIntermediaCertPath != "" {
if err := xssh.WriteRemoteString(client, d.config.OutputIntermediaCertPath, intermediaCertPEM, d.config.UseSCP); err != nil {
return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err)
}
d.logger.Info("ssl intermedia certificate file uploaded", slog.String("path", d.config.OutputIntermediaCertPath))
}
if err := xssh.WriteRemoteString(client, d.config.OutputKeyPath, privkeyPEM, d.config.UseSCP); err != nil {
return nil, fmt.Errorf("failed to upload private key file: %w", err)
}
d.logger.Info("ssl private key file uploaded", slog.String("path", d.config.OutputKeyPath))
}
case OUTPUT_FORMAT_PFX:
pfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword)
if err != nil {
return nil, fmt.Errorf("failed to transform certificate to PFX: %w", err)
}
d.logger.Info("ssl certificate transformed to pfx")
{
pfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword)
if err != nil {
return nil, fmt.Errorf("failed to transform certificate to PFX: %w", err)
}
d.logger.Info("ssl certificate transformed to pfx")
if err := xssh.WriteRemote(client, d.config.OutputCertPath, pfxData, d.config.UseSCP); err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
if err := xssh.WriteRemote(client, d.config.OutputCertPath, pfxData, d.config.UseSCP); err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
}
d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath))
}
d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath))
case OUTPUT_FORMAT_JKS:
jksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)
if err != nil {
return nil, fmt.Errorf("failed to transform certificate to JKS: %w", err)
}
d.logger.Info("ssl certificate transformed to jks")
{
jksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)
if err != nil {
return nil, fmt.Errorf("failed to transform certificate to JKS: %w", err)
}
d.logger.Info("ssl certificate transformed to jks")
if err := xssh.WriteRemote(client, d.config.OutputCertPath, jksData, d.config.UseSCP); err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
if err := xssh.WriteRemote(client, d.config.OutputCertPath, jksData, d.config.UseSCP); err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
}
d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath))
}
d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath))
default:
return nil, fmt.Errorf("unsupported output format '%s'", d.config.OutputFormat)

View File

@ -70,6 +70,7 @@ import BizDeployNodeConfigFieldsProviderQiniuKodo from "./BizDeployNodeConfigFie
import BizDeployNodeConfigFieldsProviderQiniuPili from "./BizDeployNodeConfigFieldsProviderQiniuPili";
import BizDeployNodeConfigFieldsProviderRainYunRCDN from "./BizDeployNodeConfigFieldsProviderRainYunRCDN";
import BizDeployNodeConfigFieldsProviderRatPanel from "./BizDeployNodeConfigFieldsProviderRatPanel.tsx";
import BizDeployNodeConfigFieldsProviderS3 from "./BizDeployNodeConfigFieldsProviderS3";
import BizDeployNodeConfigFieldsProviderSafeLine from "./BizDeployNodeConfigFieldsProviderSafeLine.tsx";
import BizDeployNodeConfigFieldsProviderSSH from "./BizDeployNodeConfigFieldsProviderSSH";
import BizDeployNodeConfigFieldsProviderSynologyDSM from "./BizDeployNodeConfigFieldsProviderSynologyDSM";
@ -183,6 +184,7 @@ const providerComponentMap: Partial<Record<DeploymentProviderType, React.Compone
[DEPLOYMENT_PROVIDERS.QINIU_PILI]: BizDeployNodeConfigFieldsProviderQiniuPili,
[DEPLOYMENT_PROVIDERS.RAINYUN_RCDN]: BizDeployNodeConfigFieldsProviderRainYunRCDN,
[DEPLOYMENT_PROVIDERS.RATPANEL]: BizDeployNodeConfigFieldsProviderRatPanel,
[DEPLOYMENT_PROVIDERS.S3]: BizDeployNodeConfigFieldsProviderS3,
[DEPLOYMENT_PROVIDERS.SAFELINE]: BizDeployNodeConfigFieldsProviderSafeLine,
[DEPLOYMENT_PROVIDERS.SSH]: BizDeployNodeConfigFieldsProviderSSH,
[DEPLOYMENT_PROVIDERS.SYNOLOGYDSM]: BizDeployNodeConfigFieldsProviderSynologyDSM,

View File

@ -418,8 +418,8 @@ const BizDeployNodeConfigFieldsProviderLocal = () => {
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
format: FORMAT_PEM,
certPath: "/etc/ssl/certimate/cert.crt",
keyPath: "/etc/ssl/certimate/cert.key",
certPath: "/etc/ssl/certimate/cert.crt",
shellEnv: SHELLENV_SH,
};
};

View File

@ -0,0 +1,286 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { CERTIFICATE_FORMATS } from "@/domain/certificate";
import { useFormNestedFieldsContext } from "./_context";
const FORMAT_PEM = CERTIFICATE_FORMATS.PEM;
const FORMAT_PFX = CERTIFICATE_FORMATS.PFX;
const FORMAT_JKS = CERTIFICATE_FORMATS.JKS;
const BizDeployNodeConfigFieldsProviderS3 = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance();
const initialValues = getInitialValues();
const fieldFormat = Form.useWatch([parentNamePath, "format"], formInst);
const fieldCertPath = Form.useWatch([parentNamePath, "certObjectKey"], formInst);
const handleFormatSelect = (value: string) => {
if (fieldFormat === value) return;
switch (value) {
case FORMAT_PEM:
{
if (/(.pfx|.jks)$/.test(fieldCertPath)) {
formInst.setFieldValue([parentNamePath, "certObjectKey"], fieldCertPath.replace(/(.pfx|.jks)$/, ".crt"));
}
}
break;
case FORMAT_PFX:
{
if (/(.crt|.jks)$/.test(fieldCertPath)) {
formInst.setFieldValue([parentNamePath, "certObjectKey"], fieldCertPath.replace(/(.crt|.jks)$/, ".pfx"));
}
}
break;
case FORMAT_JKS:
{
if (/(.crt|.pfx)$/.test(fieldCertPath)) {
formInst.setFieldValue([parentNamePath, "certObjectKey"], fieldCertPath.replace(/(.crt|.pfx)$/, ".jks"));
}
}
break;
}
};
return (
<>
<Form.Item
name={[parentNamePath, "region"]}
initialValue={initialValues.region}
label={t("workflow_node.deploy.form.s3_region.label")}
rules={[formRule]}
>
<Input placeholder={t("workflow_node.deploy.form.s3_region.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "bucket"]}
initialValue={initialValues.bucket}
label={t("workflow_node.deploy.form.s3_bucket.label")}
rules={[formRule]}
>
<Input placeholder={t("workflow_node.deploy.form.s3_bucket.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "format"]}
initialValue={initialValues.format}
label={t("workflow_node.deploy.form.s3_format.label")}
rules={[formRule]}
>
<Select
options={[FORMAT_PEM, FORMAT_PFX, FORMAT_JKS].map((s) => ({
key: s,
label: t(`workflow_node.deploy.form.s3_format.option.${s.toLowerCase()}.label`),
value: s,
}))}
placeholder={t("workflow_node.deploy.form.s3_format.placeholder")}
onSelect={handleFormatSelect}
/>
</Form.Item>
<Show when={fieldFormat === FORMAT_PEM}>
<Form.Item
name={[parentNamePath, "keyObjectKey"]}
initialValue={initialValues.keyObjectKey}
label={t("workflow_node.deploy.form.s3_key_object_key.label")}
rules={[formRule]}
>
<Input placeholder={t("workflow_node.deploy.form.s3_key_object_key.placeholder")} />
</Form.Item>
</Show>
<Form.Item
name={[parentNamePath, "certObjectKey"]}
initialValue={initialValues.certObjectKey}
label={t(`workflow_node.deploy.form.s3_${fieldFormat === FORMAT_PEM ? "fullchaincert" : "cert"}_object_key.label`)}
rules={[formRule]}
>
<Input placeholder={t(`workflow_node.deploy.form.s3_${fieldFormat === FORMAT_PEM ? "fullchaincert" : "cert"}_object_key.placeholder`)} />
</Form.Item>
<Show when={fieldFormat === FORMAT_PEM}>
<Form.Item
name={[parentNamePath, "certObjectKeyForServerOnly"]}
initialValue={initialValues.certObjectKeyForServerOnly}
label={t("workflow_node.deploy.form.s3_servercert_object_key.label")}
extra={t("workflow_node.deploy.form.s3_servercert_object_key.help")}
rules={[formRule]}
>
<Input allowClear placeholder={t("workflow_node.deploy.form.s3_servercert_object_key.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "certObjectKeyForIntermediaOnly"]}
initialValue={initialValues.certObjectKeyForIntermediaOnly}
label={t("workflow_node.deploy.form.s3_intermediacert_object_key.label")}
extra={t("workflow_node.deploy.form.s3_intermediacert_object_key.help")}
rules={[formRule]}
>
<Input allowClear placeholder={t("workflow_node.deploy.form.s3_intermediacert_object_key.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldFormat === FORMAT_PFX}>
<Form.Item
name={[parentNamePath, "pfxPassword"]}
initialValue={initialValues.pfxPassword}
label={t("workflow_node.deploy.form.s3_pfx_password.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.s3_pfx_password.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.s3_pfx_password.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldFormat === FORMAT_JKS}>
<Form.Item
name={[parentNamePath, "jksAlias"]}
initialValue={initialValues.jksAlias}
label={t("workflow_node.deploy.form.s3_jks_alias.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.s3_jks_alias.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.s3_jks_alias.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "jksKeypass"]}
initialValue={initialValues.jksKeypass}
label={t("workflow_node.deploy.form.s3_jks_keypass.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.s3_jks_keypass.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.s3_jks_keypass.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "jksStorepass"]}
initialValue={initialValues.jksStorepass}
label={t("workflow_node.deploy.form.s3_jks_storepass.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.s3_jks_storepass.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.s3_jks_storepass.placeholder")} />
</Form.Item>
</Show>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
region: "",
bucket: "",
format: FORMAT_PEM,
keyObjectKey: ".certimate/cert.key",
certObjectKey: ".certimate/cert.crt",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
const { t } = i18n;
return z
.object({
region: z.string().nonempty(t("workflow_node.deploy.form.s3_region.placeholder")),
bucket: z.string().nonempty(t("workflow_node.deploy.form.s3_bucket.placeholder")),
format: z.literal([FORMAT_PEM, FORMAT_PFX, FORMAT_JKS], t("workflow_node.deploy.form.s3_format.placeholder")),
keyObjectKey: z
.string()
.max(256, t("common.errmsg.string_max", { max: 256 }))
.nullish(),
certObjectKey: z
.string()
.min(1, t("workflow_node.deploy.form.s3_cert_object_key.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })),
certObjectKeyForServerOnly: z
.string()
.max(256, t("common.errmsg.string_max", { max: 256 }))
.nullish(),
certObjectKeyForIntermediaOnly: z
.string()
.max(256, t("common.errmsg.string_max", { max: 256 }))
.nullish(),
pfxPassword: z.string().nullish(),
jksAlias: z.string().nullish(),
jksKeypass: z.string().nullish(),
jksStorepass: z.string().nullish(),
})
.superRefine((values, ctx) => {
switch (values.format) {
case FORMAT_PEM:
{
if (!values.keyObjectKey?.trim()) {
ctx.addIssue({
code: "custom",
message: t("workflow_node.deploy.form.s3_key_object_key.placeholder"),
path: ["keyObjectKey"],
});
}
}
break;
case FORMAT_PFX:
{
if (!values.pfxPassword?.trim()) {
ctx.addIssue({
code: "custom",
message: t("workflow_node.deploy.form.s3_pfx_password.placeholder"),
path: ["pfxPassword"],
});
}
}
break;
case FORMAT_JKS:
{
if (!values.jksAlias?.trim()) {
ctx.addIssue({
code: "custom",
message: t("workflow_node.deploy.form.s3_jks_alias.placeholder"),
path: ["jksAlias"],
});
}
if (!values.jksKeypass?.trim()) {
ctx.addIssue({
code: "custom",
message: t("workflow_node.deploy.form.s3_jks_keypass.placeholder"),
path: ["jksKeypass"],
});
}
if (!values.jksStorepass?.trim()) {
ctx.addIssue({
code: "custom",
message: t("workflow_node.deploy.form.s3_jks_storepass.placeholder"),
path: ["jksStorepass"],
});
}
}
break;
}
});
};
const _default = Object.assign(BizDeployNodeConfigFieldsProviderS3, {
getInitialValues,
getSchema,
});
export default _default;

View File

@ -460,8 +460,8 @@ const BizDeployNodeConfigFieldsProviderSSH = () => {
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
format: FORMAT_PEM,
certPath: "/etc/ssl/certimate/cert.crt",
keyPath: "/etc/ssl/certimate/cert.key",
certPath: "/etc/ssl/certimate/cert.crt",
};
};

View File

@ -615,6 +615,7 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({
RAINYUN_RCDN: `${ACCESS_PROVIDERS.RAINYUN}-rcdn`,
RATPANEL: `${ACCESS_PROVIDERS.RATPANEL}`,
RATPANEL_CONSOLE: `${ACCESS_PROVIDERS.RATPANEL}-console`,
S3: `${ACCESS_PROVIDERS.S3}`,
SAFELINE: `${ACCESS_PROVIDERS.SAFELINE}`,
SSH: `${ACCESS_PROVIDERS.SSH}`,
SYNOLOGYDSM: `${ACCESS_PROVIDERS.SYNOLOGYDSM}`,
@ -690,6 +691,7 @@ export const deploymentProvidersMap: Map<DeploymentProvider["type"] | string, De
[DEPLOYMENT_PROVIDERS.SSH, "provider.ssh", DEPLOYMENT_CATEGORIES.OTHER],
[DEPLOYMENT_PROVIDERS.WEBHOOK, "provider.webhook", DEPLOYMENT_CATEGORIES.OTHER],
[DEPLOYMENT_PROVIDERS.KUBERNETES_SECRET, "provider.kubernetes_secret", DEPLOYMENT_CATEGORIES.OTHER],
[DEPLOYMENT_PROVIDERS.S3, "provider.s3_upload", DEPLOYMENT_CATEGORIES.STORAGE],
[DEPLOYMENT_PROVIDERS.ALIYUN_OSS, "provider.aliyun_oss", DEPLOYMENT_CATEGORIES.STORAGE],
[DEPLOYMENT_PROVIDERS.ALIYUN_CDN, "provider.aliyun_cdn", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.ALIYUN_DCDN, "provider.aliyun_dcdn", DEPLOYMENT_CATEGORIES.CDN],

View File

@ -159,6 +159,7 @@
"provider.ratpanel_console": "AcePanel - Console itself",
"provider.rfc2136": "RFC 2136: Dynamic DNS Updates",
"provider.s3": "Object storage (S3-compatible)",
"provider.s3_upload": "Upload to S3-compatible object storage",
"provider.safeline": "SafeLine",
"provider.sectigo": "Sectigo",
"provider.slackbot": "Slack Bot",

View File

@ -779,6 +779,39 @@
"workflow_node.deploy.form.ratpanel_site_names.tooltip": "You can find it on AcePanel dashboard.",
"workflow_node.deploy.form.ratpanel_site_names.multiple_input_modal.title": "Change AcePanel website names",
"workflow_node.deploy.form.ratpanel_site_names.multiple_input_modal.placeholder": "Please enter AcePanel website name",
"workflow_node.deploy.form.s3_region.label": "Object storage (S3-compatible) region",
"workflow_node.deploy.form.s3_region.placeholder": "Please enter region",
"workflow_node.deploy.form.s3_bucket.label": "Object storage (S3-compatible) bucket",
"workflow_node.deploy.form.s3_bucket.placeholder": "Please enter bucket name",
"workflow_node.deploy.form.s3_format.label": "File format",
"workflow_node.deploy.form.s3_format.placeholder": "Please select file format",
"workflow_node.deploy.form.s3_format.option.pem.label": "PEM (*.pem, *.crt, *.key)",
"workflow_node.deploy.form.s3_format.option.pfx.label": "PFX (*.pfx, *.p12)",
"workflow_node.deploy.form.s3_format.option.jks.label": "JKS (*.jks)",
"workflow_node.deploy.form.s3_key_object_key.label": "Private key file object key",
"workflow_node.deploy.form.s3_key_object_key.placeholder": "Please enter the object key for private key file",
"workflow_node.deploy.form.s3_cert_object_key.label": "Certificate file object key",
"workflow_node.deploy.form.s3_cert_object_key.placeholder": "Please enter the object key for certificate",
"workflow_node.deploy.form.s3_fullchaincert_object_key.label": "Bundled fullchain certificate object key",
"workflow_node.deploy.form.s3_fullchaincert_object_key.placeholder": "Please enter the remote path for bundled fullchain certificate",
"workflow_node.deploy.form.s3_servercert_object_key.label": "Server certificate object key (Optional)",
"workflow_node.deploy.form.s3_servercert_object_key.placeholder": "Please enter the object key for server certificate file",
"workflow_node.deploy.form.s3_servercert_object_key.help": "",
"workflow_node.deploy.form.s3_intermediacert_object_key.label": "Intermediate CA certificate object key (Optional)",
"workflow_node.deploy.form.s3_intermediacert_object_key.placeholder": "Please enter the object key for intermediate CA certificate file",
"workflow_node.deploy.form.s3_intermediacert_object_key.help": "",
"workflow_node.deploy.form.s3_pfx_password.label": "PFX password",
"workflow_node.deploy.form.s3_pfx_password.placeholder": "Please enter PFX password",
"workflow_node.deploy.form.s3_pfx_password.tooltip": "For more information, see <a href=\"https://learn.microsoft.com/en-us/windows-hardware/drivers/install/personal-information-exchange---pfx--files\" target=\"_blank\">https://learn.microsoft.com/en-us/windows-hardware/drivers/install/personal-information-exchange---pfx--files</a>",
"workflow_node.deploy.form.s3_jks_alias.label": "JKS alias",
"workflow_node.deploy.form.s3_jks_alias.placeholder": "Please enter JKS alias",
"workflow_node.deploy.form.s3_jks_alias.tooltip": "For more information, see <a href=\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\" target=\"_blank\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>",
"workflow_node.deploy.form.s3_jks_keypass.label": "JKS key password",
"workflow_node.deploy.form.s3_jks_keypass.placeholder": "Please enter JKS key password",
"workflow_node.deploy.form.s3_jks_keypass.tooltip": "For more information, see <a href=\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\" target=\"_blank\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>",
"workflow_node.deploy.form.s3_jks_storepass.label": "JKS store password",
"workflow_node.deploy.form.s3_jks_storepass.placeholder": "Please enter JKS store password",
"workflow_node.deploy.form.s3_jks_storepass.tooltip": "For more information, see <a href=\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\" target=\"_blank\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>",
"workflow_node.deploy.form.safeline_resource_type.option.certificate.label": "Certificate",
"workflow_node.deploy.form.safeline_certificate_id.label": "SafeLine certificate ID",
"workflow_node.deploy.form.safeline_certificate_id.placeholder": "Please enter SafeLine certificate ID",

View File

@ -159,6 +159,7 @@
"provider.ratpanel_console": "耗子面板 - 面板自身",
"provider.rfc2136": "RFC 2136: Dynamic DNS Updates",
"provider.s3": "对象存储S3 兼容)",
"provider.s3_upload": "上传到 S3 兼容的对象存储",
"provider.safeline": "雷池",
"provider.sectigo": "Sectigo",
"provider.slackbot": "Slack 机器人",

View File

@ -777,6 +777,39 @@
"workflow_node.deploy.form.ratpanel_site_names.tooltip": "请登录耗子面板查看",
"workflow_node.deploy.form.ratpanel_site_names.multiple_input_modal.title": "修改耗子面板网站名称",
"workflow_node.deploy.form.ratpanel_site_names.multiple_input_modal.placeholder": "请输入耗子面板网站名称",
"workflow_node.deploy.form.s3_region.label": "对象存储区域",
"workflow_node.deploy.form.s3_region.placeholder": "请输入对象存储区域",
"workflow_node.deploy.form.s3_bucket.label": "对象存储桶名",
"workflow_node.deploy.form.s3_bucket.placeholder": "请输入对象存储桶名",
"workflow_node.deploy.form.s3_format.label": "文件格式",
"workflow_node.deploy.form.s3_format.placeholder": "请选择文件格式",
"workflow_node.deploy.form.s3_format.option.pem.label": "PEM 格式(*.pem, *.crt, *.key",
"workflow_node.deploy.form.s3_format.option.pfx.label": "PFX 格式(*.pfx, *.p12",
"workflow_node.deploy.form.s3_format.option.jks.label": "JKS 格式(*.jks",
"workflow_node.deploy.form.s3_key_object_key.label": "私钥文件对象键",
"workflow_node.deploy.form.s3_key_object_key.placeholder": "请输入私钥文件对象键",
"workflow_node.deploy.form.s3_cert_object_key.label": "证书文件对象键",
"workflow_node.deploy.form.s3_cert_object_key.placeholder": "请输入证书文件对象键",
"workflow_node.deploy.form.s3_fullchaincert_object_key.label": "证书链文件对象键",
"workflow_node.deploy.form.s3_fullchaincert_object_key.placeholder": "请输入证书链文件对象键",
"workflow_node.deploy.form.s3_servercert_object_key.label": "服务器证书文件对象键(可选)",
"workflow_node.deploy.form.s3_servercert_object_key.placeholder": "请输入服务器证书文件对象键",
"workflow_node.deploy.form.s3_servercert_object_key.help": "注意:不填写时将不会上传服务器证书。",
"workflow_node.deploy.form.s3_intermediacert_object_key.label": "中间证书文件对象键(可选)",
"workflow_node.deploy.form.s3_intermediacert_object_key.placeholder": "请输入中间证书文件对象键",
"workflow_node.deploy.form.s3_intermediacert_object_key.help": "注意:不填写时将不会上传中间证书。",
"workflow_node.deploy.form.s3_pfx_password.label": "PFX 导出密码",
"workflow_node.deploy.form.s3_pfx_password.placeholder": "请输入 PFX 导出密码",
"workflow_node.deploy.form.s3_pfx_password.tooltip": "这是什么?请参阅 <a href=\"https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files\" target=\"_blank\">https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files</a>",
"workflow_node.deploy.form.s3_jks_alias.label": "JKS 别名",
"workflow_node.deploy.form.s3_jks_alias.placeholder": "请输入 JKS 别名",
"workflow_node.deploy.form.s3_jks_alias.tooltip": "这是什么?请参阅 <a href=\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\" target=\"_blank\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>",
"workflow_node.deploy.form.s3_jks_keypass.label": "JKS 私钥访问口令",
"workflow_node.deploy.form.s3_jks_keypass.placeholder": "请输入 JKS 私钥访问口令",
"workflow_node.deploy.form.s3_jks_keypass.tooltip": "这是什么?请参阅 <a href=\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\" target=\"_blank\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>",
"workflow_node.deploy.form.s3_jks_storepass.label": "JKS 密钥库存储口令",
"workflow_node.deploy.form.s3_jks_storepass.placeholder": "请输入 JKS 密钥库存储口令",
"workflow_node.deploy.form.s3_jks_storepass.tooltip": "这是什么?请参阅 <a href=\"https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html\" target=\"_blank\">https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html</a>",
"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",