diff --git a/go.mod b/go.mod index 7ce3ede6..a567360f 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 github.com/nikoksr/notify v1.0.0 + github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 github.com/pkg/sftp v1.13.6 github.com/pocketbase/dbx v1.10.1 github.com/pocketbase/pocketbase v0.22.18 @@ -32,6 +33,7 @@ require ( k8s.io/api v0.31.1 k8s.io/apimachinery v0.31.1 k8s.io/client-go v0.31.1 + software.sslmate.com/src/go-pkcs12 v0.5.0 ) require ( diff --git a/go.sum b/go.sum index 05531e6b..08ae184b 100644 --- a/go.sum +++ b/go.sum @@ -400,6 +400,8 @@ github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= +github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 h1:2nosf3P75OZv2/ZO/9Px5ZgZ5gbKrzA3joN1QMfOGMQ= +github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0/go.mod h1:lAVhWwbNaveeJmxrxuSTxMgKpF6DjnuVpn6T8WiBwYQ= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -460,7 +462,6 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017 h1:Oymmfm github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017/go.mod h1:gnLxGXlLmF+jDqWR1/RVoF/UUwxQxomQhkc0oN7KeuI= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017 h1:SXrldOXwgomYuATVAuz5ofpTjB+99qVELgdy5R5kMgI= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030 h1:kwiUoCkooUgy7iPyhEEbio7WT21kGJUeZ5JeJfb/dYk= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= @@ -780,3 +781,5 @@ sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+s sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= +software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index c26907db..9dc537f3 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -1,16 +1,21 @@ package deployer import ( + "bytes" "context" "encoding/json" + "encoding/pem" "errors" "fmt" - "strings" + "time" + "github.com/pavlo-v-chernykh/keystore-go/v4" "github.com/pocketbase/pocketbase/models" + "software.sslmate.com/src/go-pkcs12" "github.com/usual2970/certimate/internal/applicant" "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/pkg/utils/x509" "github.com/usual2970/certimate/internal/utils/app" ) @@ -38,7 +43,6 @@ const ( type DeployerOption struct { DomainId string `json:"domainId"` Domain string `json:"domain"` - Product string `json:"product"` Access string `json:"access"` AccessRecord *models.Record `json:"-"` DeployConfig domain.DeployConfig `json:"deployConfig"` @@ -90,7 +94,6 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep option := &DeployerOption{ DomainId: record.Id, Domain: record.GetString("domain"), - Product: getProduct(deployConfig.Type), Access: access.GetString("config"), AccessRecord: access, DeployConfig: deployConfig, @@ -118,7 +121,7 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep case targetAliyunNLB: return NewAliyunNLBDeployer(option) case targetTencentCDN: - return NewTencentCDNDeployer(option) + return NewTencentCDNDeployer(option) case targetTencentECDN: return NewTencentECDNDeployer(option) case targetTencentCLB: @@ -145,14 +148,6 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep return nil, errors.New("unsupported deploy target") } -func getProduct(t string) string { - rs := strings.Split(t, "-") - if len(rs) < 2 { - return "" - } - return rs[1] -} - func toStr(tag string, data any) string { if data == nil { return tag @@ -194,4 +189,58 @@ func getDeployVariables(conf domain.DeployConfig) map[string]string { } return rs -} \ No newline at end of file +} + +func convertPEMToPFX(certificate string, privateKey string, password string) ([]byte, error) { + cert, err := x509.ParseCertificateFromPEM(certificate) + if err != nil { + return nil, err + } + + privkey, err := x509.ParsePKCS1PrivateKeyFromPEM(privateKey) + if err != nil { + return nil, err + } + + pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, password) + if err != nil { + return nil, fmt.Errorf("failed to encode as pfx %w", err) + } + + return pfxData, nil +} + +func convertPEMToJKS(certificate string, privateKey string, alias string, keypass string, storepass string) ([]byte, error) { + certBlock, _ := pem.Decode([]byte(certificate)) + if certBlock == nil { + return nil, errors.New("failed to decode certificate PEM") + } + + privkeyBlock, _ := pem.Decode([]byte(privateKey)) + if privkeyBlock == nil { + return nil, errors.New("failed to decode private key PEM") + } + + ks := keystore.New() + entry := keystore.PrivateKeyEntry{ + CreationTime: time.Now(), + PrivateKey: privkeyBlock.Bytes, + CertificateChain: []keystore.Certificate{ + { + Type: "X509", + Content: certBlock.Bytes, + }, + }, + } + + if err := ks.SetPrivateKeyEntry(alias, entry, []byte(keypass)); err != nil { + return nil, err + } + + var buf bytes.Buffer + if err := ks.Store(&buf, []byte(storepass)); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/internal/deployer/local.go b/internal/deployer/local.go index 784660b6..0c839fa4 100644 --- a/internal/deployer/local.go +++ b/internal/deployer/local.go @@ -1,15 +1,15 @@ package deployer import ( + "bytes" "context" "encoding/json" "fmt" - "os" "os/exec" - "path/filepath" "runtime" "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/pkg/utils/fs" ) type LocalDeployer struct { @@ -17,6 +17,18 @@ type LocalDeployer struct { infos []string } +const ( + certFormatPEM = "pem" + certFormatPFX = "pfx" + certFormatJKS = "jks" +) + +const ( + shellEnvSh = "sh" + shellEnvCmd = "cmd" + shellEnvPowershell = "powershell" +) + func NewLocalDeployer(option *DeployerOption) (Deployer, error) { return &LocalDeployer{ option: option, @@ -38,74 +50,114 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error { return err } - preCommand := getDeployString(d.option.DeployConfig, "preCommand") - + // 执行前置命令 + preCommand := d.option.DeployConfig.GetConfigAsString("preCommand") if preCommand != "" { - if err := execCmd(preCommand); err != nil { - return fmt.Errorf("执行前置命令失败: %w", err) + stdout, stderr, err := d.execCommand(preCommand) + if err != nil { + return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr) } + + d.infos = append(d.infos, toStr("执行前置命令成功", stdout)) } - // 复制证书文件 - if err := copyFile(getDeployString(d.option.DeployConfig, "certPath"), d.option.Certificate.Certificate); err != nil { - return fmt.Errorf("复制证书失败: %w", err) - } + // 写入证书和私钥文件 + switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) { + case certFormatPEM: + if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil { + return fmt.Errorf("failed to save certificate file: %w", err) + } - // 复制私钥文件 - if err := copyFile(getDeployString(d.option.DeployConfig, "keyPath"), d.option.Certificate.PrivateKey); err != nil { - return fmt.Errorf("复制私钥失败: %w", err) + d.infos = append(d.infos, toStr("保存证书成功", nil)) + + if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("keyPath"), d.option.Certificate.PrivateKey); err != nil { + return fmt.Errorf("failed to save private key file: %w", err) + } + + d.infos = append(d.infos, toStr("保存私钥成功", nil)) + + case certFormatPFX: + pfxData, err := convertPEMToPFX( + d.option.Certificate.Certificate, + d.option.Certificate.PrivateKey, + d.option.DeployConfig.GetConfigAsString("pfxPassword"), + ) + if err != nil { + return fmt.Errorf("failed to convert pem to pfx %w", err) + } + + if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil { + return fmt.Errorf("failed to save certificate file: %w", err) + } + + d.infos = append(d.infos, toStr("保存证书成功", nil)) + + case certFormatJKS: + jksData, err := convertPEMToJKS( + d.option.Certificate.Certificate, + d.option.Certificate.PrivateKey, + d.option.DeployConfig.GetConfigAsString("jksAlias"), + d.option.DeployConfig.GetConfigAsString("jksKeypass"), + d.option.DeployConfig.GetConfigAsString("jksStorepass"), + ) + if err != nil { + return fmt.Errorf("failed to convert pem to pfx %w", err) + } + + if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil { + return fmt.Errorf("failed to save certificate file: %w", err) + } + + d.infos = append(d.infos, toStr("保存证书成功", nil)) } // 执行命令 - if err := execCmd(getDeployString(d.option.DeployConfig, "command")); err != nil { - return fmt.Errorf("执行命令失败: %w", err) + command := d.option.DeployConfig.GetConfigAsString("command") + if command != "" { + stdout, stderr, err := d.execCommand(command) + if err != nil { + return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr) + } + + d.infos = append(d.infos, toStr("执行命令成功", stdout)) } return nil } -func execCmd(command string) error { - // 执行命令 +func (d *LocalDeployer) execCommand(command string) (string, string, error) { var cmd *exec.Cmd - if runtime.GOOS == "windows" { - cmd = exec.Command("cmd", "/C", command) - } else { + switch d.option.DeployConfig.GetConfigAsString("shell") { + case shellEnvSh: cmd = exec.Command("sh", "-c", command) + + case shellEnvCmd: + cmd = exec.Command("cmd", "/C", command) + + case shellEnvPowershell: + cmd = exec.Command("powershell", "-Command", command) + + case "": + if runtime.GOOS == "windows" { + cmd = exec.Command("cmd", "/C", command) + } else { + cmd = exec.Command("sh", "-c", command) + } + + default: + return "", "", fmt.Errorf("unsupported shell") } - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + var stdoutBuf bytes.Buffer + cmd.Stdout = &stdoutBuf + var stderrBuf bytes.Buffer + cmd.Stderr = &stderrBuf err := cmd.Run() if err != nil { - return fmt.Errorf("执行命令失败: %w", err) + return "", "", fmt.Errorf("failed to execute script: %w", err) } - return nil -} - -func copyFile(path string, content string) error { - dir := filepath.Dir(path) - - // 如果目录不存在,创建目录 - err := os.MkdirAll(dir, os.ModePerm) - if err != nil { - return fmt.Errorf("创建目录失败: %w", err) - } - - // 创建或打开文件 - file, err := os.Create(path) - if err != nil { - return fmt.Errorf("创建文件失败: %w", err) - } - defer file.Close() - - // 写入内容到文件 - _, err = file.Write([]byte(content)) - if err != nil { - return fmt.Errorf("写入文件失败: %w", err) - } - - return nil + return stdoutBuf.String(), stderrBuf.String(), err } diff --git a/internal/deployer/qiniu_cdn_test.go b/internal/deployer/qiniu_cdn_test.go index ca236550..67012bfc 100644 --- a/internal/deployer/qiniu_cdn_test.go +++ b/internal/deployer/qiniu_cdn_test.go @@ -24,7 +24,6 @@ func Test_qiuniu_uploadCert(t *testing.T) { option: &DeployerOption{ DomainId: "1", Domain: "example.com", - Product: "test", Access: `{"bucket":"test","accessKey":"","secretKey":""}`, Certificate: applicant.Certificate{ Certificate: "", @@ -70,7 +69,6 @@ func Test_qiuniu_modifyDomainCert(t *testing.T) { option: &DeployerOption{ DomainId: "1", Domain: "jt1.ikit.fun", - Product: "test", Access: `{"bucket":"test","accessKey":"","secretKey":""}`, }, }, diff --git a/internal/deployer/ssh.go b/internal/deployer/ssh.go index 551e8634..98c4de48 100644 --- a/internal/deployer/ssh.go +++ b/internal/deployer/ssh.go @@ -6,12 +6,13 @@ import ( "encoding/json" "fmt" "os" - xpath "path" + "path/filepath" "github.com/pkg/sftp" - sshPkg "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh" "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/pkg/utils/fs" ) type SSHDeployer struct { @@ -41,49 +42,120 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error { } // 连接 - client, err := d.createClient(access) + client, err := d.createSshClient(access) if err != nil { return err } defer client.Close() - d.infos = append(d.infos, toStr("ssh连接成功", nil)) + d.infos = append(d.infos, toStr("SSH 连接成功", nil)) // 执行前置命令 - preCommand := getDeployString(d.option.DeployConfig, "preCommand") + preCommand := d.option.DeployConfig.GetConfigAsString("preCommand") if preCommand != "" { stdout, stderr, err := d.sshExecCommand(client, preCommand) if err != nil { return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr) } + + d.infos = append(d.infos, toStr("SSH 执行前置命令成功", stdout)) } - // 上传证书 - if err := d.upload(client, d.option.Certificate.Certificate, getDeployString(d.option.DeployConfig, "certPath")); err != nil { - return fmt.Errorf("failed to upload certificate: %w", err) + // 上传证书和私钥文件 + switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) { + case certFormatPEM: + if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil { + return fmt.Errorf("failed to upload certificate file: %w", err) + } + + d.infos = append(d.infos, toStr("SSH 上传证书成功", nil)) + + if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("keyPath"), d.option.Certificate.PrivateKey); err != nil { + return fmt.Errorf("failed to upload private key file: %w", err) + } + + d.infos = append(d.infos, toStr("SSH 上传私钥成功", nil)) + + case certFormatPFX: + pfxData, err := convertPEMToPFX( + d.option.Certificate.Certificate, + d.option.Certificate.PrivateKey, + d.option.DeployConfig.GetConfigAsString("pfxPassword"), + ) + if err != nil { + return fmt.Errorf("failed to convert pem to pfx %w", err) + } + + if err := d.writeSftpFile(client, d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil { + return fmt.Errorf("failed to upload certificate file: %w", err) + } + + d.infos = append(d.infos, toStr("SSH 上传证书成功", nil)) + + case certFormatJKS: + jksData, err := convertPEMToJKS( + d.option.Certificate.Certificate, + d.option.Certificate.PrivateKey, + d.option.DeployConfig.GetConfigAsString("jksAlias"), + d.option.DeployConfig.GetConfigAsString("jksKeypass"), + d.option.DeployConfig.GetConfigAsString("jksStorepass"), + ) + if err != nil { + return fmt.Errorf("failed to convert pem to pfx %w", err) + } + + if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil { + return fmt.Errorf("failed to save certificate file: %w", err) + } + + d.infos = append(d.infos, toStr("保存证书成功", nil)) } - d.infos = append(d.infos, toStr("ssh上传证书成功", nil)) - - // 上传私钥 - if err := d.upload(client, d.option.Certificate.PrivateKey, getDeployString(d.option.DeployConfig, "keyPath")); err != nil { - return fmt.Errorf("failed to upload private key: %w", err) - } - - d.infos = append(d.infos, toStr("ssh上传私钥成功", nil)) - // 执行命令 - stdout, stderr, err := d.sshExecCommand(client, getDeployString(d.option.DeployConfig, "command")) - if err != nil { - return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr) - } + command := d.option.DeployConfig.GetConfigAsString("command") + if command != "" { + stdout, stderr, err := d.sshExecCommand(client, command) + if err != nil { + return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr) + } - d.infos = append(d.infos, toStr("ssh执行命令成功", stdout)) + d.infos = append(d.infos, toStr("SSH 执行命令成功", stdout)) + } return nil } -func (d *SSHDeployer) sshExecCommand(client *sshPkg.Client, command string) (string, string, error) { +func (d *SSHDeployer) createSshClient(access *domain.SSHAccess) (*ssh.Client, error) { + var authMethod ssh.AuthMethod + + if access.Key != "" { + var signer ssh.Signer + var err error + + if access.KeyPassphrase != "" { + signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(access.Key), []byte(access.KeyPassphrase)) + } else { + signer, err = ssh.ParsePrivateKey([]byte(access.Key)) + } + + if err != nil { + return nil, err + } + authMethod = ssh.PublicKeys(signer) + } else { + authMethod = ssh.Password(access.Password) + } + + return ssh.Dial("tcp", fmt.Sprintf("%s:%s", access.Host, access.Port), &ssh.ClientConfig{ + User: access.Username, + Auth: []ssh.AuthMethod{ + authMethod, + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + }) +} + +func (d *SSHDeployer) sshExecCommand(client *ssh.Client, command string) (string, string, error) { session, err := client.NewSession() if err != nil { return "", "", fmt.Errorf("failed to create ssh session: %w", err) @@ -98,14 +170,18 @@ func (d *SSHDeployer) sshExecCommand(client *sshPkg.Client, command string) (str return stdoutBuf.String(), stderrBuf.String(), err } -func (d *SSHDeployer) upload(client *sshPkg.Client, content, path string) error { +func (d *SSHDeployer) writeSftpFileString(client *ssh.Client, path string, content string) error { + return d.writeSftpFile(client, path, []byte(content)) +} + +func (d *SSHDeployer) writeSftpFile(client *ssh.Client, path string, data []byte) error { sftpCli, err := sftp.NewClient(client) if err != nil { return fmt.Errorf("failed to create sftp client: %w", err) } defer sftpCli.Close() - if err := sftpCli.MkdirAll(xpath.Dir(path)); err != nil { + if err := sftpCli.MkdirAll(filepath.Dir(path)); err != nil { return fmt.Errorf("failed to create remote directory: %w", err) } @@ -115,40 +191,10 @@ func (d *SSHDeployer) upload(client *sshPkg.Client, content, path string) error } defer file.Close() - _, err = file.Write([]byte(content)) + _, err = file.Write(data) if err != nil { return fmt.Errorf("failed to write to remote file: %w", err) } return nil } - -func (d *SSHDeployer) createClient(access *domain.SSHAccess) (*sshPkg.Client, error) { - var authMethod sshPkg.AuthMethod - - if access.Key != "" { - var signer sshPkg.Signer - var err error - - if access.KeyPassphrase != "" { - signer, err = sshPkg.ParsePrivateKeyWithPassphrase([]byte(access.Key), []byte(access.KeyPassphrase)) - } else { - signer, err = sshPkg.ParsePrivateKey([]byte(access.Key)) - } - - if err != nil { - return nil, err - } - authMethod = sshPkg.PublicKeys(signer) - } else { - authMethod = sshPkg.Password(access.Password) - } - - return sshPkg.Dial("tcp", fmt.Sprintf("%s:%s", access.Host, access.Port), &sshPkg.ClientConfig{ - User: access.Username, - Auth: []sshPkg.AuthMethod{ - authMethod, - }, - HostKeyCallback: sshPkg.InsecureIgnoreHostKey(), - }) -} diff --git a/internal/pkg/utils/fs/fs.go b/internal/pkg/utils/fs/fs.go new file mode 100644 index 00000000..3ae82060 --- /dev/null +++ b/internal/pkg/utils/fs/fs.go @@ -0,0 +1,51 @@ +package fs + +import ( + "fmt" + "os" + "path/filepath" +) + +// 与 [WriteFile] 类似,但写入的是字符串内容。 +// +// 入参: +// - path: 文件路径。 +// - content: 文件内容。 +// +// 出参: +// - 错误。 +func WriteFileString(path string, content string) error { + return WriteFile(path, []byte(content)) +} + +// 将数据写入指定路径的文件。 +// 如果目录不存在,将会递归创建目录。 +// 如果文件不存在,将会创建该文件;如果文件已存在,将会覆盖原有内容。 +// +// 入参: +// - path: 文件路径。 +// - data: 文件数据字节数组。 +// +// 出参: +// - 错误。 +func WriteFile(path string, data []byte) error { + dir := filepath.Dir(path) + + err := os.MkdirAll(dir, os.ModePerm) + if err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + file, err := os.Create(path) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer file.Close() + + _, err = file.Write(data) + if err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + return nil +} diff --git a/internal/pkg/utils/x509/x509.go b/internal/pkg/utils/x509/x509.go index 09d67d3a..40cc39d6 100644 --- a/internal/pkg/utils/x509/x509.go +++ b/internal/pkg/utils/x509/x509.go @@ -2,6 +2,7 @@ import ( "crypto/ecdsa" + "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" @@ -48,7 +49,7 @@ func ParseCertificateFromPEM(certPem string) (cert *x509.Certificate, err error) return cert, nil } -// 从 PEM 编码的私钥字符串解析并返回一个 ECDSA 私钥对象。 +// 从 PEM 编码的私钥字符串解析并返回一个 ecdsa.PrivateKey 对象。 // // 入参: // - privkeyPem: 私钥 PEM 内容。 @@ -72,7 +73,31 @@ func ParseECPrivateKeyFromPEM(privkeyPem string) (privkey *ecdsa.PrivateKey, err return privkey, nil } -// 将 ECDSA 私钥转换为 PEM 编码的字符串。 +// 从 PEM 编码的私钥字符串解析并返回一个 rsa.PrivateKey 对象。 +// +// 入参: +// - privkeyPem: 私钥 PEM 内容。 +// +// 出参: +// - privkey: rsa.PrivateKey 对象。 +// - err: 错误。 +func ParsePKCS1PrivateKeyFromPEM(privkeyPem string) (privkey *rsa.PrivateKey, err error) { + pemData := []byte(privkeyPem) + + block, _ := pem.Decode(pemData) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + + privkey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + + return privkey, nil +} + +// 将 ecdsa.PrivateKey 对象转换为 PEM 编码的字符串。 // // 入参: // - privkey: ecdsa.PrivateKey 对象。 diff --git a/ui/src/components/certimate/DeployEditDialog.tsx b/ui/src/components/certimate/DeployEditDialog.tsx index 87a63673..3e4804b9 100644 --- a/ui/src/components/certimate/DeployEditDialog.tsx +++ b/ui/src/components/certimate/DeployEditDialog.tsx @@ -21,6 +21,7 @@ import DeployToTencentTEO from "./DeployToTencentTEO"; import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN"; import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB"; import DeployToQiniuCDN from "./DeployToQiniuCDN"; +import DeployToLocal from "./DeployToLocal"; import DeployToSSH from "./DeployToSSH"; import DeployToWebhook from "./DeployToWebhook"; import DeployToKubernetesSecret from "./DeployToKubernetesSecret"; @@ -153,8 +154,10 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro case "qiniu-cdn": childComponent = ; break; - case "ssh": case "local": + childComponent = ; + break; + case "ssh": childComponent = ; break; case "webhook": diff --git a/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx b/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx index b85203e8..e7a7fc4a 100644 --- a/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx +++ b/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx @@ -102,7 +102,7 @@ const DeployToHuaweiCloudCDN = () => { onValueChange={(value) => { const newData = produce(data, (draft) => { draft.config ??= {}; - draft.config.resourceType = value?.trim(); + draft.config.resourceType = value; }); setDeploy(newData); }} diff --git a/ui/src/components/certimate/DeployToLocal.tsx b/ui/src/components/certimate/DeployToLocal.tsx new file mode 100644 index 00000000..672cf52c --- /dev/null +++ b/ui/src/components/certimate/DeployToLocal.tsx @@ -0,0 +1,450 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { produce } from "immer"; + +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { useDeployEditContext } from "./DeployEdit"; +import { cn } from "@/lib/utils"; + +const DeployToLocal = () => { + const { t } = useTranslation(); + + const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + + useEffect(() => { + if (!data.id) { + setDeploy({ + ...data, + config: { + format: "pem", + certPath: "/etc/nginx/ssl/nginx.crt", + keyPath: "/etc/nginx/ssl/nginx.key", + pfxPassword: "", + jksAlias: "", + jksKeypass: "", + jksStorepass: "", + shell: "sh", + preCommand: "", + command: "sudo service nginx reload", + }, + }); + } + }, []); + + useEffect(() => { + setError({}); + }, []); + + const formSchema = z + .object({ + format: z.union([z.literal("pem"), z.literal("pfx"), z.literal("jks")], { + message: t("domain.deployment.form.file_format.placeholder"), + }), + certPath: z + .string() + .min(1, t("domain.deployment.form.file_cert_path.placeholder")) + .max(255, t("common.errmsg.string_max", { max: 255 })), + keyPath: z + .string() + .min(0, t("domain.deployment.form.file_key_path.placeholder")) + .max(255, t("common.errmsg.string_max", { max: 255 })), + pfxPassword: z.string().optional(), + jksAlias: z.string().optional(), + jksKeypass: z.string().optional(), + jksStorepass: z.string().optional(), + shell: z.union([z.literal("sh"), z.literal("cmd"), z.literal("powershell")], { + message: t("domain.deployment.form.shell.placeholder"), + }), + preCommand: z.string().optional(), + command: z.string().optional(), + }) + .refine((data) => (data.format === "pem" ? !!data.keyPath?.trim() : true), { + message: t("domain.deployment.form.file_key_path.placeholder"), + path: ["keyPath"], + }) + .refine((data) => (data.format === "pfx" ? !!data.pfxPassword?.trim() : true), { + message: t("domain.deployment.form.file_pfx_password.placeholder"), + path: ["pfxPassword"], + }) + .refine((data) => (data.format === "jks" ? !!data.jksAlias?.trim() : true), { + message: t("domain.deployment.form.file_jks_alias.placeholder"), + path: ["jksAlias"], + }) + .refine((data) => (data.format === "jks" ? !!data.jksKeypass?.trim() : true), { + message: t("domain.deployment.form.file_jks_keypass.placeholder"), + path: ["jksKeypass"], + }) + .refine((data) => (data.format === "jks" ? !!data.jksStorepass?.trim() : true), { + message: t("domain.deployment.form.file_jks_storepass.placeholder"), + path: ["jksStorepass"], + }); + + useEffect(() => { + const res = formSchema.safeParse(data.config); + if (!res.success) { + setError({ + ...error, + format: res.error.errors.find((e) => e.path[0] === "format")?.message, + certPath: res.error.errors.find((e) => e.path[0] === "certPath")?.message, + keyPath: res.error.errors.find((e) => e.path[0] === "keyPath")?.message, + pfxPassword: res.error.errors.find((e) => e.path[0] === "pfxPassword")?.message, + jksAlias: res.error.errors.find((e) => e.path[0] === "jksAlias")?.message, + jksKeypass: res.error.errors.find((e) => e.path[0] === "jksKeypass")?.message, + jksStorepass: res.error.errors.find((e) => e.path[0] === "jksStorepass")?.message, + shell: res.error.errors.find((e) => e.path[0] === "shell")?.message, + preCommand: res.error.errors.find((e) => e.path[0] === "preCommand")?.message, + command: res.error.errors.find((e) => e.path[0] === "command")?.message, + }); + } else { + setError({ + ...error, + format: undefined, + certPath: undefined, + keyPath: undefined, + pfxPassword: undefined, + jksAlias: undefined, + jksKeypass: undefined, + jksStorepass: undefined, + shell: undefined, + preCommand: undefined, + command: undefined, + }); + } + }, [data]); + + useEffect(() => { + if (data.config?.format === "pem") { + if (/(.pfx|.jks)$/.test(data.config.certPath)) { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.certPath = data.config!.certPath.replace(/(.pfx|.jks)$/, ".crt"); + }); + setDeploy(newData); + } + } else if (data.config?.format === "pfx") { + if (/(.crt|.jks)$/.test(data.config.certPath)) { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.certPath = data.config!.certPath.replace(/(.crt|.jks)$/, ".pfx"); + }); + setDeploy(newData); + } + } else if (data.config?.format === "jks") { + if (/(.crt|.pfx)$/.test(data.config.certPath)) { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.certPath = data.config!.certPath.replace(/(.crt|.pfx)$/, ".jks"); + }); + setDeploy(newData); + } + } + }, [data.config?.format]); + + const getOptionCls = (val: string) => { + if (data.config?.shell === val) { + return "border-primary dark:border-primary"; + } + + return ""; + }; + + const handleUsePresetScript = (key: string) => { + switch (key) { + case "reload_nginx": + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.shell = "sh"; + draft.config.command = "sudo service nginx reload"; + }); + setDeploy(newData); + } + break; + + case "binding_iis": + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.shell = "powershell"; + draft.config.command = ` +# 请将以下变量替换为实际值 +$pfxPath = "" # PFX 文件路径 +$pfxPassword = "" # PFX 密码 +$siteName = "" # IIS 网站名称 +$domain = "" # 域名 +$ipaddr = "" # 绑定 IP,“*”表示所有 IP 绑定 +$port = "" # 绑定端口 + + +# 导入证书到本地计算机的个人存储区 +$cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable +# 获取 Thumbprint +$thumbprint = $cert.Thumbprint +# 导入 WebAdministration 模块 +Import-Module WebAdministration +# 检查是否已存在 HTTPS 绑定 +$existingBinding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -HostHeader "$domain" -ErrorAction SilentlyContinue +if (!$existingBinding) { + # 添加新的 HTTPS 绑定 + New-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain" +} +# 获取绑定对象 +$binding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain" +# 绑定 SSL 证书 +$binding.AddSslCertificate($thumbprint, "My") +# 删除目录下的证书文件 +Remove-Item -Path "$pfxPath" -Force + `.trim(); + }); + setDeploy(newData); + } + break; + } + }; + + return ( + <> +
+
+ + +
{error?.format}
+
+ +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.certPath = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.certPath}
+
+ + {data.config?.format === "pem" ? ( +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.keyPath = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.keyPath}
+
+ ) : ( + <> + )} + + {data.config?.format === "pfx" ? ( +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.pfxPassword = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.pfxPassword}
+
+ ) : ( + <> + )} + + {data.config?.format === "jks" ? ( + <> +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.jksAlias = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.jksAlias}
+
+ +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.jksKeypass = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.jksKeypass}
+
+ +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.jksStorepass = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.jksStorepass}
+
+ + ) : ( + <> + )} + +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.shell = val; + }); + setDeploy(newData); + }} + > +
+ + +
+
+ + +
+
+ + +
+
+
{error?.shell}
+
+ +
+ + +
{error?.preCommand}
+
+ +
+
+ + + + {t("domain.deployment.form.shell_preset_scripts.trigger")} + + + handleUsePresetScript("reload_nginx")}> + {t("domain.deployment.form.shell_preset_scripts.option.reload_nginx.label")} + + handleUsePresetScript("binding_iis")}> + {t("domain.deployment.form.shell_preset_scripts.option.binding_iis.label")} + + + +
+ +
{error?.command}
+
+
+ + ); +}; + +export default DeployToLocal; diff --git a/ui/src/components/certimate/DeployToSSH.tsx b/ui/src/components/certimate/DeployToSSH.tsx index 80eb15fb..b59eeb72 100644 --- a/ui/src/components/certimate/DeployToSSH.tsx +++ b/ui/src/components/certimate/DeployToSSH.tsx @@ -1,29 +1,31 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; +import { z } from "zod"; import { produce } from "immer"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { useDeployEditContext } from "./DeployEdit"; const DeployToSSH = () => { const { t } = useTranslation(); - const { setError } = useDeployEditContext(); - useEffect(() => { - setError({}); - }, []); - - const { deploy: data, setDeploy } = useDeployEditContext(); + const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); useEffect(() => { if (!data.id) { setDeploy({ ...data, config: { + format: "pem", certPath: "/etc/nginx/ssl/nginx.crt", keyPath: "/etc/nginx/ssl/nginx.key", + pfxPassword: "", + jksAlias: "", + jksKeypass: "", + jksStorepass: "", preCommand: "", command: "sudo service nginx reload", }, @@ -31,79 +33,287 @@ const DeployToSSH = () => { } }, []); + useEffect(() => { + setError({}); + }, []); + + const formSchema = z + .object({ + format: z.union([z.literal("pem"), z.literal("pfx"), z.literal("jks")], { + message: t("domain.deployment.form.file_format.placeholder"), + }), + certPath: z + .string() + .min(1, t("domain.deployment.form.file_cert_path.placeholder")) + .max(255, t("common.errmsg.string_max", { max: 255 })), + keyPath: z + .string() + .min(0, t("domain.deployment.form.file_key_path.placeholder")) + .max(255, t("common.errmsg.string_max", { max: 255 })), + pfxPassword: z.string().optional(), + jksAlias: z.string().optional(), + jksKeypass: z.string().optional(), + jksStorepass: z.string().optional(), + preCommand: z.string().optional(), + command: z.string().optional(), + }) + .refine((data) => (data.format === "pem" ? !!data.keyPath?.trim() : true), { + message: t("domain.deployment.form.file_key_path.placeholder"), + path: ["keyPath"], + }) + .refine((data) => (data.format === "pfx" ? !!data.pfxPassword?.trim() : true), { + message: t("domain.deployment.form.file_pfx_password.placeholder"), + path: ["pfxPassword"], + }) + .refine((data) => (data.format === "jks" ? !!data.jksAlias?.trim() : true), { + message: t("domain.deployment.form.file_jks_alias.placeholder"), + path: ["jksAlias"], + }) + .refine((data) => (data.format === "jks" ? !!data.jksKeypass?.trim() : true), { + message: t("domain.deployment.form.file_jks_keypass.placeholder"), + path: ["jksKeypass"], + }) + .refine((data) => (data.format === "jks" ? !!data.jksStorepass?.trim() : true), { + message: t("domain.deployment.form.file_jks_storepass.placeholder"), + path: ["jksStorepass"], + }); + + useEffect(() => { + const res = formSchema.safeParse(data.config); + if (!res.success) { + setError({ + ...error, + format: res.error.errors.find((e) => e.path[0] === "format")?.message, + certPath: res.error.errors.find((e) => e.path[0] === "certPath")?.message, + keyPath: res.error.errors.find((e) => e.path[0] === "keyPath")?.message, + pfxPassword: res.error.errors.find((e) => e.path[0] === "pfxPassword")?.message, + jksAlias: res.error.errors.find((e) => e.path[0] === "jksAlias")?.message, + jksKeypass: res.error.errors.find((e) => e.path[0] === "jksKeypass")?.message, + jksStorepass: res.error.errors.find((e) => e.path[0] === "jksStorepass")?.message, + preCommand: res.error.errors.find((e) => e.path[0] === "preCommand")?.message, + command: res.error.errors.find((e) => e.path[0] === "command")?.message, + }); + } else { + setError({ + ...error, + format: undefined, + certPath: undefined, + keyPath: undefined, + pfxPassword: undefined, + jksAlias: undefined, + jksKeypass: undefined, + jksStorepass: undefined, + preCommand: undefined, + command: undefined, + }); + } + }, [data]); + + useEffect(() => { + if (data.config?.format === "pem") { + if (/(.pfx|.jks)$/.test(data.config.certPath)) { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.certPath = data.config!.certPath.replace(/(.pfx|.jks)$/, ".crt"); + }); + setDeploy(newData); + } + } else if (data.config?.format === "pfx") { + if (/(.crt|.jks)$/.test(data.config.certPath)) { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.certPath = data.config!.certPath.replace(/(.crt|.jks)$/, ".pfx"); + }); + setDeploy(newData); + } + } else if (data.config?.format === "jks") { + if (/(.crt|.pfx)$/.test(data.config.certPath)) { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.certPath = data.config!.certPath.replace(/(.crt|.pfx)$/, ".jks"); + }); + setDeploy(newData); + } + } + }, [data.config?.format]); + return ( <>
- + + +
{error?.format}
+
+ +
+ { const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.certPath = e.target.value; + draft.config ??= {}; + draft.config.certPath = e.target.value?.trim(); }); setDeploy(newData); }} /> +
{error?.certPath}
-
- - { - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.keyPath = e.target.value; - }); - setDeploy(newData); - }} - /> -
+ {data.config?.format === "pem" ? ( +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.keyPath = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.keyPath}
+
+ ) : ( + <> + )} + + {data.config?.format === "pfx" ? ( +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.pfxPassword = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.pfxPassword}
+
+ ) : ( + <> + )} + + {data.config?.format === "jks" ? ( + <> +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.jksAlias = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.jksAlias}
+
+ +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.jksKeypass = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.jksKeypass}
+
+ +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.jksStorepass = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.jksStorepass}
+
+ + ) : ( + <> + )}
- + +
{error?.preCommand}
- + +
{error?.command}
diff --git a/ui/src/i18n/locales/en/nls.domain.json b/ui/src/i18n/locales/en/nls.domain.json index c3a5a4b1..06e78c6d 100644 --- a/ui/src/i18n/locales/en/nls.domain.json +++ b/ui/src/i18n/locales/en/nls.domain.json @@ -120,14 +120,32 @@ "domain.deployment.form.huaweicloud_elb_loadbalancer_id.placeholder": "Please enter ELB loadbalancer ID", "domain.deployment.form.huaweicloud_elb_listener_id.label": "Listener ID", "domain.deployment.form.huaweicloud_elb_listener_id.placeholder": "Please enter ELB listener ID", - "domain.deployment.form.ssh_key_path.label": "Private Key Save Path", - "domain.deployment.form.ssh_key_path.placeholder": "Please enter private key save path", - "domain.deployment.form.ssh_cert_path.label": "Certificate Save Path", - "domain.deployment.form.ssh_cert_path.placeholder": "Please enter certificate save path", - "domain.deployment.form.ssh_pre_command.label": "Pre-deployment Command", - "domain.deployment.form.ssh_pre_command.placeholder": "Command to be executed before deploying the certificate", - "domain.deployment.form.ssh_command.label": "Command", - "domain.deployment.form.ssh_command.placeholder": "Please enter command", + "domain.deployment.form.file_format.label": "Certificate Format", + "domain.deployment.form.file_format.placeholder": "Please select certificate format", + "domain.deployment.form.file_cert_path.label": "Certificate Save Path", + "domain.deployment.form.file_cert_path.placeholder": "Please enter certificate save path", + "domain.deployment.form.file_key_path.label": "Private Key Save Path", + "domain.deployment.form.file_key_path.placeholder": "Please enter private key save path", + "domain.deployment.form.file_pfx_password.label": "PFX Output Password", + "domain.deployment.form.file_pfx_password.placeholder": "Please enter PFX output password", + "domain.deployment.form.file_jks_alias.label": "JKS Alias (KeyStore Alias)", + "domain.deployment.form.file_jks_alias.placeholder": "Please enter JKS alias", + "domain.deployment.form.file_jks_keypass.label": "JKS Key Password (KeyStore Keypass)", + "domain.deployment.form.file_jks_keypass.placeholder": "Please enter JKS key password", + "domain.deployment.form.file_jks_storepass.label": "JKS Store Password (KeyStore Storepass)", + "domain.deployment.form.file_jks_storepass.placeholder": "Please enter JKS store password", + "domain.deployment.form.shell.label": "Shell", + "domain.deployment.form.shell.placeholder": "Please select shell environment", + "domain.deployment.form.shell.option.sh.label": "POSIX Bash (Linux)", + "domain.deployment.form.shell.option.cmd.label": "CMD (Windows)", + "domain.deployment.form.shell.option.powershell.label": "PowerShell (Windows)", + "domain.deployment.form.shell_pre_command.label": "Pre-deployment Command", + "domain.deployment.form.shell_pre_command.placeholder": "Command to be executed before deploying the certificate", + "domain.deployment.form.shell_command.label": "Command", + "domain.deployment.form.shell_command.placeholder": "Please enter command", + "domain.deployment.form.shell_preset_scripts.trigger": "Use Preset Scripts", + "domain.deployment.form.shell_preset_scripts.option.reload_nginx.label": "Bash - Reload Nginx", + "domain.deployment.form.shell_preset_scripts.option.binding_iis.label": "PowerShell - Binding IIS", "domain.deployment.form.k8s_namespace.label": "Namespace", "domain.deployment.form.k8s_namespace.placeholder": "Please enter namespace", "domain.deployment.form.k8s_secret_name.label": "Secret Name", diff --git a/ui/src/i18n/locales/zh/nls.domain.json b/ui/src/i18n/locales/zh/nls.domain.json index f6433bf4..7ccf94ba 100644 --- a/ui/src/i18n/locales/zh/nls.domain.json +++ b/ui/src/i18n/locales/zh/nls.domain.json @@ -104,8 +104,8 @@ "domain.deployment.form.tencent_clb_domain.label": "部署到域名(支持泛域名)", "domain.deployment.form.tencent_clb_domain.placeholder": "请输入部署到的域名, 如未开启 SNI, 可置空忽略此项", "domain.deployment.form.tencent_teo_zone_id.label": "Zone ID", - "domain.deployment.form.tencent_teo_zone_id.placeholder": "请输入zoneid, 形如: zone-xxxxxxxxx", - "domain.deployment.form.tencent_teo_domain.label": "部署到域名(支持泛域名, 应与服务器上配置的域名完全一致, 每行一个域名)", + "domain.deployment.form.tencent_teo_zone_id.placeholder": "请输入 Zone ID", + "domain.deployment.form.tencent_teo_domain.label": "部署到域名(支持泛域名, 应与服务器上配置的域名完全一致, 每行一个域名)", "domain.deployment.form.tencent_teo_domain.placeholder": "请输入部署到的域名", "domain.deployment.form.huaweicloud_elb_region.label": "地域", "domain.deployment.form.huaweicloud_elb_region.placeholder": "请输入地域(如 cn-north-1)", @@ -120,14 +120,29 @@ "domain.deployment.form.huaweicloud_elb_loadbalancer_id.placeholder": "请输入负载均衡器 ID", "domain.deployment.form.huaweicloud_elb_listener_id.label": "监听器 ID", "domain.deployment.form.huaweicloud_elb_listener_id.placeholder": "请输入监听器 ID", - "domain.deployment.form.ssh_key_path.label": "私钥保存路径", - "domain.deployment.form.ssh_key_path.placeholder": "请输入私钥保存路径", - "domain.deployment.form.ssh_cert_path.label": "证书保存路径", - "domain.deployment.form.ssh_cert_path.placeholder": "请输入证书保存路径", - "domain.deployment.form.ssh_pre_command.label": "前置命令", - "domain.deployment.form.ssh_pre_command.placeholder": "在部署证书前执行的命令", - "domain.deployment.form.ssh_command.label": "命令", - "domain.deployment.form.ssh_command.placeholder": "请输入要执行的命令", + "domain.deployment.form.file_format.label": "证书格式", + "domain.deployment.form.file_format.placeholder": "请选择证书格式", + "domain.deployment.form.file_cert_path.label": "证书保存路径", + "domain.deployment.form.file_cert_path.placeholder": "请输入证书保存路径", + "domain.deployment.form.file_key_path.label": "私钥保存路径", + "domain.deployment.form.file_key_path.placeholder": "请输入私钥保存路径", + "domain.deployment.form.file_pfx_password.label": "PFX 导出密码", + "domain.deployment.form.file_pfx_password.placeholder": "请输入 PFX 导出密码", + "domain.deployment.form.file_jks_alias.label": "JKS 别名(KeyStore Alias)", + "domain.deployment.form.file_jks_alias.placeholder": "请输入 JKS 别名", + "domain.deployment.form.file_jks_keypass.label": "JKS 私钥访问口令(KeyStore Keypass)", + "domain.deployment.form.file_jks_keypass.placeholder": "请输入 JKS 私钥访问口令", + "domain.deployment.form.file_jks_storepass.label": "JKS 密钥库存储口令(KeyStore Storepass)", + "domain.deployment.form.file_jks_storepass.placeholder": "请输入 JKS 密钥库存储口令", + "domain.deployment.form.shell.label": "Shell", + "domain.deployment.form.shell.placeholder": "请选择命令执行环境", + "domain.deployment.form.shell_pre_command.label": "前置命令", + "domain.deployment.form.shell_pre_command.placeholder": "在部署证书前执行的命令", + "domain.deployment.form.shell_command.label": "命令", + "domain.deployment.form.shell_command.placeholder": "请输入要执行的命令", + "domain.deployment.form.shell_preset_scripts.trigger": "使用预设脚本", + "domain.deployment.form.shell_preset_scripts.option.reload_nginx.label": "Bash - 重启 nginx", + "domain.deployment.form.shell_preset_scripts.option.binding_iis.label": "PowerShell - 导入并绑定到 IIS(需管理员权限)", "domain.deployment.form.k8s_namespace.label": "命名空间", "domain.deployment.form.k8s_namespace.placeholder": "请输入 K8S 命名空间", "domain.deployment.form.k8s_secret_name.label": "Secret 名称", diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx index a792e756..ff655b98 100644 --- a/ui/src/pages/dashboard/Dashboard.tsx +++ b/ui/src/pages/dashboard/Dashboard.tsx @@ -209,7 +209,7 @@ const Dashboard = () => { {t("history.log")} -
+
{deployment.log.check && ( <> {deployment.log.check.map((item: Log) => { diff --git a/ui/src/pages/history/History.tsx b/ui/src/pages/history/History.tsx index 24de0c7b..e0a47186 100644 --- a/ui/src/pages/history/History.tsx +++ b/ui/src/pages/history/History.tsx @@ -104,7 +104,7 @@ const History = () => { {t("history.log")} -
+
{deployment.log.check && ( <> {deployment.log.check.map((item: Log) => {