mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-09 14:09:52 +00:00
Merge branch 'fudiwei-feat/multiple-certificate-formats'
This commit is contained in:
commit
edeac86f06
2
go.mod
2
go.mod
@ -19,6 +19,7 @@ require (
|
|||||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114
|
||||||
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
|
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
|
||||||
github.com/nikoksr/notify v1.0.0
|
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/pkg/sftp v1.13.6
|
||||||
github.com/pocketbase/dbx v1.10.1
|
github.com/pocketbase/dbx v1.10.1
|
||||||
github.com/pocketbase/pocketbase v0.22.18
|
github.com/pocketbase/pocketbase v0.22.18
|
||||||
@ -32,6 +33,7 @@ require (
|
|||||||
k8s.io/api v0.31.1
|
k8s.io/api v0.31.1
|
||||||
k8s.io/apimachinery v0.31.1
|
k8s.io/apimachinery v0.31.1
|
||||||
k8s.io/client-go v0.31.1
|
k8s.io/client-go v0.31.1
|
||||||
|
software.sslmate.com/src/go-pkcs12 v0.5.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
5
go.sum
5
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/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 h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
|
||||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
|
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/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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
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/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.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.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.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 h1:kwiUoCkooUgy7iPyhEEbio7WT21kGJUeZ5JeJfb/dYk=
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
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/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 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
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=
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
package deployer
|
package deployer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pavlo-v-chernykh/keystore-go/v4"
|
||||||
"github.com/pocketbase/pocketbase/models"
|
"github.com/pocketbase/pocketbase/models"
|
||||||
|
"software.sslmate.com/src/go-pkcs12"
|
||||||
|
|
||||||
"github.com/usual2970/certimate/internal/applicant"
|
"github.com/usual2970/certimate/internal/applicant"
|
||||||
"github.com/usual2970/certimate/internal/domain"
|
"github.com/usual2970/certimate/internal/domain"
|
||||||
|
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||||
"github.com/usual2970/certimate/internal/utils/app"
|
"github.com/usual2970/certimate/internal/utils/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,7 +43,6 @@ const (
|
|||||||
type DeployerOption struct {
|
type DeployerOption struct {
|
||||||
DomainId string `json:"domainId"`
|
DomainId string `json:"domainId"`
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
Product string `json:"product"`
|
|
||||||
Access string `json:"access"`
|
Access string `json:"access"`
|
||||||
AccessRecord *models.Record `json:"-"`
|
AccessRecord *models.Record `json:"-"`
|
||||||
DeployConfig domain.DeployConfig `json:"deployConfig"`
|
DeployConfig domain.DeployConfig `json:"deployConfig"`
|
||||||
@ -90,7 +94,6 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
|
|||||||
option := &DeployerOption{
|
option := &DeployerOption{
|
||||||
DomainId: record.Id,
|
DomainId: record.Id,
|
||||||
Domain: record.GetString("domain"),
|
Domain: record.GetString("domain"),
|
||||||
Product: getProduct(deployConfig.Type),
|
|
||||||
Access: access.GetString("config"),
|
Access: access.GetString("config"),
|
||||||
AccessRecord: access,
|
AccessRecord: access,
|
||||||
DeployConfig: deployConfig,
|
DeployConfig: deployConfig,
|
||||||
@ -145,14 +148,6 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
|
|||||||
return nil, errors.New("unsupported deploy target")
|
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 {
|
func toStr(tag string, data any) string {
|
||||||
if data == nil {
|
if data == nil {
|
||||||
return tag
|
return tag
|
||||||
@ -195,3 +190,57 @@ func getDeployVariables(conf domain.DeployConfig) map[string]string {
|
|||||||
|
|
||||||
return rs
|
return rs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
package deployer
|
package deployer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/usual2970/certimate/internal/domain"
|
"github.com/usual2970/certimate/internal/domain"
|
||||||
|
"github.com/usual2970/certimate/internal/pkg/utils/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LocalDeployer struct {
|
type LocalDeployer struct {
|
||||||
@ -17,6 +17,18 @@ type LocalDeployer struct {
|
|||||||
infos []string
|
infos []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
certFormatPEM = "pem"
|
||||||
|
certFormatPFX = "pfx"
|
||||||
|
certFormatJKS = "jks"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
shellEnvSh = "sh"
|
||||||
|
shellEnvCmd = "cmd"
|
||||||
|
shellEnvPowershell = "powershell"
|
||||||
|
)
|
||||||
|
|
||||||
func NewLocalDeployer(option *DeployerOption) (Deployer, error) {
|
func NewLocalDeployer(option *DeployerOption) (Deployer, error) {
|
||||||
return &LocalDeployer{
|
return &LocalDeployer{
|
||||||
option: option,
|
option: option,
|
||||||
@ -38,74 +50,114 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
preCommand := getDeployString(d.option.DeployConfig, "preCommand")
|
// 执行前置命令
|
||||||
|
preCommand := d.option.DeployConfig.GetConfigAsString("preCommand")
|
||||||
if preCommand != "" {
|
if preCommand != "" {
|
||||||
if err := execCmd(preCommand); err != nil {
|
stdout, stderr, err := d.execCommand(preCommand)
|
||||||
return fmt.Errorf("执行前置命令失败: %w", err)
|
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 {
|
switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) {
|
||||||
return fmt.Errorf("复制证书失败: %w", err)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
// 复制私钥文件
|
d.infos = append(d.infos, toStr("保存证书成功", nil))
|
||||||
if err := copyFile(getDeployString(d.option.DeployConfig, "keyPath"), d.option.Certificate.PrivateKey); err != nil {
|
|
||||||
return fmt.Errorf("复制私钥失败: %w", err)
|
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 {
|
command := d.option.DeployConfig.GetConfigAsString("command")
|
||||||
return fmt.Errorf("执行命令失败: %w", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func execCmd(command string) error {
|
func (d *LocalDeployer) execCommand(command string) (string, string, error) {
|
||||||
// 执行命令
|
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
switch d.option.DeployConfig.GetConfigAsString("shell") {
|
||||||
cmd = exec.Command("cmd", "/C", command)
|
case shellEnvSh:
|
||||||
} else {
|
|
||||||
cmd = exec.Command("sh", "-c", command)
|
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
|
var stdoutBuf bytes.Buffer
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stdout = &stdoutBuf
|
||||||
|
var stderrBuf bytes.Buffer
|
||||||
|
cmd.Stderr = &stderrBuf
|
||||||
|
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("执行命令失败: %w", err)
|
return "", "", fmt.Errorf("failed to execute script: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return stdoutBuf.String(), stderrBuf.String(), err
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,6 @@ func Test_qiuniu_uploadCert(t *testing.T) {
|
|||||||
option: &DeployerOption{
|
option: &DeployerOption{
|
||||||
DomainId: "1",
|
DomainId: "1",
|
||||||
Domain: "example.com",
|
Domain: "example.com",
|
||||||
Product: "test",
|
|
||||||
Access: `{"bucket":"test","accessKey":"","secretKey":""}`,
|
Access: `{"bucket":"test","accessKey":"","secretKey":""}`,
|
||||||
Certificate: applicant.Certificate{
|
Certificate: applicant.Certificate{
|
||||||
Certificate: "",
|
Certificate: "",
|
||||||
@ -70,7 +69,6 @@ func Test_qiuniu_modifyDomainCert(t *testing.T) {
|
|||||||
option: &DeployerOption{
|
option: &DeployerOption{
|
||||||
DomainId: "1",
|
DomainId: "1",
|
||||||
Domain: "jt1.ikit.fun",
|
Domain: "jt1.ikit.fun",
|
||||||
Product: "test",
|
|
||||||
Access: `{"bucket":"test","accessKey":"","secretKey":""}`,
|
Access: `{"bucket":"test","accessKey":"","secretKey":""}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -6,12 +6,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
xpath "path"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/pkg/sftp"
|
"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/domain"
|
||||||
|
"github.com/usual2970/certimate/internal/pkg/utils/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SSHDeployer struct {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer client.Close()
|
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 != "" {
|
if preCommand != "" {
|
||||||
stdout, stderr, err := d.sshExecCommand(client, preCommand)
|
stdout, stderr, err := d.sshExecCommand(client, preCommand)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
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 {
|
switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) {
|
||||||
return fmt.Errorf("failed to upload certificate: %w", err)
|
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"))
|
command := d.option.DeployConfig.GetConfigAsString("command")
|
||||||
if err != nil {
|
if command != "" {
|
||||||
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
|
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
|
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()
|
session, err := client.NewSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("failed to create ssh session: %w", err)
|
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
|
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)
|
sftpCli, err := sftp.NewClient(client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create sftp client: %w", err)
|
return fmt.Errorf("failed to create sftp client: %w", err)
|
||||||
}
|
}
|
||||||
defer sftpCli.Close()
|
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)
|
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()
|
defer file.Close()
|
||||||
|
|
||||||
_, err = file.Write([]byte(content))
|
_, err = file.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write to remote file: %w", err)
|
return fmt.Errorf("failed to write to remote file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
51
internal/pkg/utils/fs/fs.go
Normal file
51
internal/pkg/utils/fs/fs.go
Normal file
@ -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
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -48,7 +49,7 @@ func ParseCertificateFromPEM(certPem string) (cert *x509.Certificate, err error)
|
|||||||
return cert, nil
|
return cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 PEM 编码的私钥字符串解析并返回一个 ECDSA 私钥对象。
|
// 从 PEM 编码的私钥字符串解析并返回一个 ecdsa.PrivateKey 对象。
|
||||||
//
|
//
|
||||||
// 入参:
|
// 入参:
|
||||||
// - privkeyPem: 私钥 PEM 内容。
|
// - privkeyPem: 私钥 PEM 内容。
|
||||||
@ -72,7 +73,31 @@ func ParseECPrivateKeyFromPEM(privkeyPem string) (privkey *ecdsa.PrivateKey, err
|
|||||||
return privkey, nil
|
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 对象。
|
// - privkey: ecdsa.PrivateKey 对象。
|
||||||
|
@ -21,6 +21,7 @@ import DeployToTencentTEO from "./DeployToTencentTEO";
|
|||||||
import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN";
|
import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN";
|
||||||
import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB";
|
import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB";
|
||||||
import DeployToQiniuCDN from "./DeployToQiniuCDN";
|
import DeployToQiniuCDN from "./DeployToQiniuCDN";
|
||||||
|
import DeployToLocal from "./DeployToLocal";
|
||||||
import DeployToSSH from "./DeployToSSH";
|
import DeployToSSH from "./DeployToSSH";
|
||||||
import DeployToWebhook from "./DeployToWebhook";
|
import DeployToWebhook from "./DeployToWebhook";
|
||||||
import DeployToKubernetesSecret from "./DeployToKubernetesSecret";
|
import DeployToKubernetesSecret from "./DeployToKubernetesSecret";
|
||||||
@ -153,8 +154,10 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
|
|||||||
case "qiniu-cdn":
|
case "qiniu-cdn":
|
||||||
childComponent = <DeployToQiniuCDN />;
|
childComponent = <DeployToQiniuCDN />;
|
||||||
break;
|
break;
|
||||||
case "ssh":
|
|
||||||
case "local":
|
case "local":
|
||||||
|
childComponent = <DeployToLocal />;
|
||||||
|
break;
|
||||||
|
case "ssh":
|
||||||
childComponent = <DeployToSSH />;
|
childComponent = <DeployToSSH />;
|
||||||
break;
|
break;
|
||||||
case "webhook":
|
case "webhook":
|
||||||
|
@ -102,7 +102,7 @@ const DeployToHuaweiCloudCDN = () => {
|
|||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
const newData = produce(data, (draft) => {
|
const newData = produce(data, (draft) => {
|
||||||
draft.config ??= {};
|
draft.config ??= {};
|
||||||
draft.config.resourceType = value?.trim();
|
draft.config.resourceType = value;
|
||||||
});
|
});
|
||||||
setDeploy(newData);
|
setDeploy(newData);
|
||||||
}}
|
}}
|
||||||
|
450
ui/src/components/certimate/DeployToLocal.tsx
Normal file
450
ui/src/components/certimate/DeployToLocal.tsx
Normal file
@ -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 = "<your-pfx-path>" # PFX 文件路径
|
||||||
|
$pfxPassword = "<your-pfx-password>" # PFX 密码
|
||||||
|
$siteName = "<your-site-name>" # IIS 网站名称
|
||||||
|
$domain = "<your-domain-name>" # 域名
|
||||||
|
$ipaddr = "<your-binding-ip>" # 绑定 IP,“*”表示所有 IP 绑定
|
||||||
|
$port = "<your-binding-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 (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col space-y-8">
|
||||||
|
<div>
|
||||||
|
<Label>{t("domain.deployment.form.file_format.label")}</Label>
|
||||||
|
<Select
|
||||||
|
value={data?.config?.format}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
draft.config ??= {};
|
||||||
|
draft.config.format = value;
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={t("domain.deployment.form.file_format.placeholder")} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value="pem">PEM</SelectItem>
|
||||||
|
<SelectItem value="pfx">PFX</SelectItem>
|
||||||
|
<SelectItem value="jks">JKS</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.format}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>{t("domain.deployment.form.file_cert_path.label")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("domain.deployment.form.file_cert_path.label")}
|
||||||
|
className="w-full mt-1"
|
||||||
|
value={data?.config?.certPath}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
draft.config ??= {};
|
||||||
|
draft.config.certPath = e.target.value?.trim();
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.certPath}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data.config?.format === "pem" ? (
|
||||||
|
<div>
|
||||||
|
<Label>{t("domain.deployment.form.file_key_path.label")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("domain.deployment.form.file_key_path.placeholder")}
|
||||||
|
className="w-full mt-1"
|
||||||
|
value={data?.config?.keyPath}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
draft.config ??= {};
|
||||||
|
draft.config.keyPath = e.target.value?.trim();
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.keyPath}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.config?.format === "pfx" ? (
|
||||||
|
<div>
|
||||||
|
<Label>{t("domain.deployment.form.file_pfx_password.label")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("domain.deployment.form.file_pfx_password.placeholder")}
|
||||||
|
className="w-full mt-1"
|
||||||
|
value={data?.config?.pfxPassword}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
draft.config ??= {};
|
||||||
|
draft.config.pfxPassword = e.target.value?.trim();
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.pfxPassword}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.config?.format === "jks" ? (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Label>{t("domain.deployment.form.file_jks_alias.label")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("domain.deployment.form.file_jks_alias.placeholder")}
|
||||||
|
className="w-full mt-1"
|
||||||
|
value={data?.config?.jksAlias}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
draft.config ??= {};
|
||||||
|
draft.config.jksAlias = e.target.value?.trim();
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.jksAlias}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>{t("domain.deployment.form.file_jks_keypass.label")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("domain.deployment.form.file_jks_keypass.placeholder")}
|
||||||
|
className="w-full mt-1"
|
||||||
|
value={data?.config?.jksKeypass}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
draft.config ??= {};
|
||||||
|
draft.config.jksKeypass = e.target.value?.trim();
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.jksKeypass}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>{t("domain.deployment.form.file_jks_storepass.label")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("domain.deployment.form.file_jks_storepass.placeholder")}
|
||||||
|
className="w-full mt-1"
|
||||||
|
value={data?.config?.jksStorepass}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
draft.config ??= {};
|
||||||
|
draft.config.jksStorepass = e.target.value?.trim();
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.jksStorepass}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>{t("domain.deployment.form.shell.label")}</Label>
|
||||||
|
<RadioGroup
|
||||||
|
className="flex mt-1"
|
||||||
|
value={data?.config?.shell}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
draft.config ??= {};
|
||||||
|
draft.config.shell = val;
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="sh" id="shellOptionSh" />
|
||||||
|
<Label htmlFor="shellOptionSh">
|
||||||
|
<div className={cn("flex items-center space-x-2 border p-2 rounded cursor-pointer dark:border-stone-700", getOptionCls("sh"))}>
|
||||||
|
<div>POSIX Bash (Linux)</div>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="cmd" id="shellOptionCmd" />
|
||||||
|
<Label htmlFor="shellOptionCmd">
|
||||||
|
<div className={cn("border p-2 rounded cursor-pointer dark:border-stone-700", getOptionCls("cmd"))}>
|
||||||
|
<div>CMD (Windows)</div>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="powershell" id="shellOptionPowerShell" />
|
||||||
|
<Label htmlFor="shellOptionPowerShell">
|
||||||
|
<div className={cn("border p-2 rounded cursor-pointer dark:border-stone-700", getOptionCls("powershell"))}>
|
||||||
|
<div>PowerShell (Windows)</div>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.shell}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>{t("domain.deployment.form.shell_pre_command.label")}</Label>
|
||||||
|
<Textarea
|
||||||
|
className="mt-1"
|
||||||
|
value={data?.config?.preCommand}
|
||||||
|
placeholder={t("domain.deployment.form.shell_pre_command.placeholder")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
draft.config ??= {};
|
||||||
|
draft.config.preCommand = e.target.value;
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
></Textarea>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.preCommand}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>{t("domain.deployment.form.shell_command.label")}</Label>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<a className="text-xs text-blue-500 cursor-pointer">{t("domain.deployment.form.shell_preset_scripts.trigger")}</a>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem onClick={() => handleUsePresetScript("reload_nginx")}>
|
||||||
|
{t("domain.deployment.form.shell_preset_scripts.option.reload_nginx.label")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleUsePresetScript("binding_iis")}>
|
||||||
|
{t("domain.deployment.form.shell_preset_scripts.option.binding_iis.label")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
className="mt-1"
|
||||||
|
value={data?.config?.command}
|
||||||
|
placeholder={t("domain.deployment.form.shell_command.placeholder")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
draft.config ??= {};
|
||||||
|
draft.config.command = e.target.value;
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
></Textarea>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.command}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeployToLocal;
|
@ -1,29 +1,31 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { z } from "zod";
|
||||||
import { produce } from "immer";
|
import { produce } from "immer";
|
||||||
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { useDeployEditContext } from "./DeployEdit";
|
import { useDeployEditContext } from "./DeployEdit";
|
||||||
|
|
||||||
const DeployToSSH = () => {
|
const DeployToSSH = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setError } = useDeployEditContext();
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
|
||||||
setError({});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { deploy: data, setDeploy } = useDeployEditContext();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data.id) {
|
if (!data.id) {
|
||||||
setDeploy({
|
setDeploy({
|
||||||
...data,
|
...data,
|
||||||
config: {
|
config: {
|
||||||
|
format: "pem",
|
||||||
certPath: "/etc/nginx/ssl/nginx.crt",
|
certPath: "/etc/nginx/ssl/nginx.crt",
|
||||||
keyPath: "/etc/nginx/ssl/nginx.key",
|
keyPath: "/etc/nginx/ssl/nginx.key",
|
||||||
|
pfxPassword: "",
|
||||||
|
jksAlias: "",
|
||||||
|
jksKeypass: "",
|
||||||
|
jksStorepass: "",
|
||||||
preCommand: "",
|
preCommand: "",
|
||||||
command: "sudo service nginx reload",
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col space-y-8">
|
<div className="flex flex-col space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<Label>{t("domain.deployment.form.ssh_cert_path.label")}</Label>
|
<Label>{t("domain.deployment.form.file_format.label")}</Label>
|
||||||
|
<Select
|
||||||
|
value={data?.config?.format}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
draft.config ??= {};
|
||||||
|
draft.config.format = value;
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={t("domain.deployment.form.file_format.placeholder")} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value="pem">PEM</SelectItem>
|
||||||
|
<SelectItem value="pfx">PFX</SelectItem>
|
||||||
|
<SelectItem value="jks">JKS</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.format}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>{t("domain.deployment.form.file_cert_path.label")}</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("domain.deployment.form.ssh_cert_path.label")}
|
placeholder={t("domain.deployment.form.file_cert_path.label")}
|
||||||
className="w-full mt-1"
|
className="w-full mt-1"
|
||||||
value={data?.config?.certPath}
|
value={data?.config?.certPath}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newData = produce(data, (draft) => {
|
const newData = produce(data, (draft) => {
|
||||||
if (!draft.config) {
|
draft.config ??= {};
|
||||||
draft.config = {};
|
draft.config.certPath = e.target.value?.trim();
|
||||||
}
|
|
||||||
draft.config.certPath = e.target.value;
|
|
||||||
});
|
});
|
||||||
setDeploy(newData);
|
setDeploy(newData);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.certPath}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{data.config?.format === "pem" ? (
|
||||||
<Label>{t("domain.deployment.form.ssh_key_path.label")}</Label>
|
<div>
|
||||||
<Input
|
<Label>{t("domain.deployment.form.file_key_path.label")}</Label>
|
||||||
placeholder={t("domain.deployment.form.ssh_key_path.placeholder")}
|
<Input
|
||||||
className="w-full mt-1"
|
placeholder={t("domain.deployment.form.file_key_path.placeholder")}
|
||||||
value={data?.config?.keyPath}
|
className="w-full mt-1"
|
||||||
onChange={(e) => {
|
value={data?.config?.keyPath}
|
||||||
const newData = produce(data, (draft) => {
|
onChange={(e) => {
|
||||||
if (!draft.config) {
|
const newData = produce(data, (draft) => {
|
||||||
draft.config = {};
|
draft.config ??= {};
|
||||||
}
|
draft.config.keyPath = e.target.value?.trim();
|
||||||
draft.config.keyPath = e.target.value;
|
});
|
||||||
});
|
setDeploy(newData);
|
||||||
setDeploy(newData);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
<div className="text-red-600 text-sm mt-1">{error?.keyPath}</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.config?.format === "pfx" ? (
|
||||||
|
<div>
|
||||||
|
<Label>{t("domain.deployment.form.file_pfx_password.label")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("domain.deployment.form.file_pfx_password.placeholder")}
|
||||||
|
className="w-full mt-1"
|
||||||
|
value={data?.config?.pfxPassword}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
draft.config ??= {};
|
||||||
|
draft.config.pfxPassword = e.target.value?.trim();
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.pfxPassword}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.config?.format === "jks" ? (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Label>{t("domain.deployment.form.file_jks_alias.label")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("domain.deployment.form.file_jks_alias.placeholder")}
|
||||||
|
className="w-full mt-1"
|
||||||
|
value={data?.config?.jksAlias}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
draft.config ??= {};
|
||||||
|
draft.config.jksAlias = e.target.value?.trim();
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.jksAlias}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>{t("domain.deployment.form.file_jks_keypass.label")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("domain.deployment.form.file_jks_keypass.placeholder")}
|
||||||
|
className="w-full mt-1"
|
||||||
|
value={data?.config?.jksKeypass}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
draft.config ??= {};
|
||||||
|
draft.config.jksKeypass = e.target.value?.trim();
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.jksKeypass}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>{t("domain.deployment.form.file_jks_storepass.label")}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder={t("domain.deployment.form.file_jks_storepass.placeholder")}
|
||||||
|
className="w-full mt-1"
|
||||||
|
value={data?.config?.jksStorepass}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = produce(data, (draft) => {
|
||||||
|
draft.config ??= {};
|
||||||
|
draft.config.jksStorepass = e.target.value?.trim();
|
||||||
|
});
|
||||||
|
setDeploy(newData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.jksStorepass}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>{t("domain.deployment.form.ssh_pre_command.label")}</Label>
|
<Label>{t("domain.deployment.form.shell_pre_command.label")}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
value={data?.config?.preCommand}
|
value={data?.config?.preCommand}
|
||||||
placeholder={t("domain.deployment.form.ssh_pre_command.placeholder")}
|
placeholder={t("domain.deployment.form.shell_pre_command.placeholder")}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newData = produce(data, (draft) => {
|
const newData = produce(data, (draft) => {
|
||||||
if (!draft.config) {
|
draft.config ??= {};
|
||||||
draft.config = {};
|
|
||||||
}
|
|
||||||
draft.config.preCommand = e.target.value;
|
draft.config.preCommand = e.target.value;
|
||||||
});
|
});
|
||||||
setDeploy(newData);
|
setDeploy(newData);
|
||||||
}}
|
}}
|
||||||
></Textarea>
|
></Textarea>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.preCommand}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>{t("domain.deployment.form.ssh_command.label")}</Label>
|
<Label>{t("domain.deployment.form.shell_command.label")}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
value={data?.config?.command}
|
value={data?.config?.command}
|
||||||
placeholder={t("domain.deployment.form.ssh_command.placeholder")}
|
placeholder={t("domain.deployment.form.shell_command.placeholder")}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newData = produce(data, (draft) => {
|
const newData = produce(data, (draft) => {
|
||||||
if (!draft.config) {
|
draft.config ??= {};
|
||||||
draft.config = {};
|
|
||||||
}
|
|
||||||
draft.config.command = e.target.value;
|
draft.config.command = e.target.value;
|
||||||
});
|
});
|
||||||
setDeploy(newData);
|
setDeploy(newData);
|
||||||
}}
|
}}
|
||||||
></Textarea>
|
></Textarea>
|
||||||
|
<div className="text-red-600 text-sm mt-1">{error?.command}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -120,14 +120,32 @@
|
|||||||
"domain.deployment.form.huaweicloud_elb_loadbalancer_id.placeholder": "Please enter ELB loadbalancer ID",
|
"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.label": "Listener ID",
|
||||||
"domain.deployment.form.huaweicloud_elb_listener_id.placeholder": "Please enter ELB 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.file_format.label": "Certificate Format",
|
||||||
"domain.deployment.form.ssh_key_path.placeholder": "Please enter private key save path",
|
"domain.deployment.form.file_format.placeholder": "Please select certificate format",
|
||||||
"domain.deployment.form.ssh_cert_path.label": "Certificate Save Path",
|
"domain.deployment.form.file_cert_path.label": "Certificate Save Path",
|
||||||
"domain.deployment.form.ssh_cert_path.placeholder": "Please enter certificate save path",
|
"domain.deployment.form.file_cert_path.placeholder": "Please enter certificate save path",
|
||||||
"domain.deployment.form.ssh_pre_command.label": "Pre-deployment Command",
|
"domain.deployment.form.file_key_path.label": "Private Key Save Path",
|
||||||
"domain.deployment.form.ssh_pre_command.placeholder": "Command to be executed before deploying the certificate",
|
"domain.deployment.form.file_key_path.placeholder": "Please enter private key save path",
|
||||||
"domain.deployment.form.ssh_command.label": "Command",
|
"domain.deployment.form.file_pfx_password.label": "PFX Output Password",
|
||||||
"domain.deployment.form.ssh_command.placeholder": "Please enter command",
|
"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.label": "Namespace",
|
||||||
"domain.deployment.form.k8s_namespace.placeholder": "Please enter namespace",
|
"domain.deployment.form.k8s_namespace.placeholder": "Please enter namespace",
|
||||||
"domain.deployment.form.k8s_secret_name.label": "Secret Name",
|
"domain.deployment.form.k8s_secret_name.label": "Secret Name",
|
||||||
|
@ -104,8 +104,8 @@
|
|||||||
"domain.deployment.form.tencent_clb_domain.label": "部署到域名(支持泛域名)",
|
"domain.deployment.form.tencent_clb_domain.label": "部署到域名(支持泛域名)",
|
||||||
"domain.deployment.form.tencent_clb_domain.placeholder": "请输入部署到的域名, 如未开启 SNI, 可置空忽略此项",
|
"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.label": "Zone ID",
|
||||||
"domain.deployment.form.tencent_teo_zone_id.placeholder": "请输入zoneid, 形如: zone-xxxxxxxxx",
|
"domain.deployment.form.tencent_teo_zone_id.placeholder": "请输入 Zone ID",
|
||||||
"domain.deployment.form.tencent_teo_domain.label": "部署到域名(支持泛域名, 应与服务器上配置的域名完全一致, 每行一个域名)",
|
"domain.deployment.form.tencent_teo_domain.label": "部署到域名(支持泛域名, 应与服务器上配置的域名完全一致, 每行一个域名)",
|
||||||
"domain.deployment.form.tencent_teo_domain.placeholder": "请输入部署到的域名",
|
"domain.deployment.form.tencent_teo_domain.placeholder": "请输入部署到的域名",
|
||||||
"domain.deployment.form.huaweicloud_elb_region.label": "地域",
|
"domain.deployment.form.huaweicloud_elb_region.label": "地域",
|
||||||
"domain.deployment.form.huaweicloud_elb_region.placeholder": "请输入地域(如 cn-north-1)",
|
"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_loadbalancer_id.placeholder": "请输入负载均衡器 ID",
|
||||||
"domain.deployment.form.huaweicloud_elb_listener_id.label": "监听器 ID",
|
"domain.deployment.form.huaweicloud_elb_listener_id.label": "监听器 ID",
|
||||||
"domain.deployment.form.huaweicloud_elb_listener_id.placeholder": "请输入监听器 ID",
|
"domain.deployment.form.huaweicloud_elb_listener_id.placeholder": "请输入监听器 ID",
|
||||||
"domain.deployment.form.ssh_key_path.label": "私钥保存路径",
|
"domain.deployment.form.file_format.label": "证书格式",
|
||||||
"domain.deployment.form.ssh_key_path.placeholder": "请输入私钥保存路径",
|
"domain.deployment.form.file_format.placeholder": "请选择证书格式",
|
||||||
"domain.deployment.form.ssh_cert_path.label": "证书保存路径",
|
"domain.deployment.form.file_cert_path.label": "证书保存路径",
|
||||||
"domain.deployment.form.ssh_cert_path.placeholder": "请输入证书保存路径",
|
"domain.deployment.form.file_cert_path.placeholder": "请输入证书保存路径",
|
||||||
"domain.deployment.form.ssh_pre_command.label": "前置命令",
|
"domain.deployment.form.file_key_path.label": "私钥保存路径",
|
||||||
"domain.deployment.form.ssh_pre_command.placeholder": "在部署证书前执行的命令",
|
"domain.deployment.form.file_key_path.placeholder": "请输入私钥保存路径",
|
||||||
"domain.deployment.form.ssh_command.label": "命令",
|
"domain.deployment.form.file_pfx_password.label": "PFX 导出密码",
|
||||||
"domain.deployment.form.ssh_command.placeholder": "请输入要执行的命令",
|
"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.label": "命名空间",
|
||||||
"domain.deployment.form.k8s_namespace.placeholder": "请输入 K8S 命名空间",
|
"domain.deployment.form.k8s_namespace.placeholder": "请输入 K8S 命名空间",
|
||||||
"domain.deployment.form.k8s_secret_name.label": "Secret 名称",
|
"domain.deployment.form.k8s_secret_name.label": "Secret 名称",
|
||||||
|
@ -209,7 +209,7 @@ const Dashboard = () => {
|
|||||||
{t("history.log")}
|
{t("history.log")}
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">
|
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh] overflow-y-auto">
|
||||||
{deployment.log.check && (
|
{deployment.log.check && (
|
||||||
<>
|
<>
|
||||||
{deployment.log.check.map((item: Log) => {
|
{deployment.log.check.map((item: Log) => {
|
||||||
|
@ -104,7 +104,7 @@ const History = () => {
|
|||||||
{t("history.log")}
|
{t("history.log")}
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">
|
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh] overflow-y-auto">
|
||||||
{deployment.log.check && (
|
{deployment.log.check && (
|
||||||
<>
|
<>
|
||||||
{deployment.log.check.map((item: Log) => {
|
{deployment.log.check.map((item: Log) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user