mirror of https://github.com/usual2970/certimate
				
				
				
			feat: support specified shell on deployment to local
							parent
							
								
									332c5c5127
								
							
						
					
					
						commit
						e7870e2b05
					
				| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -38,74 +38,84 @@ 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", "pem") {
 | 
			
		||||
	case "pfx":
 | 
			
		||||
		// TODO: pfx
 | 
			
		||||
		return fmt.Errorf("not implemented")
 | 
			
		||||
 | 
			
		||||
	// 复制私钥文件
 | 
			
		||||
	if err := copyFile(getDeployString(d.option.DeployConfig, "keyPath"), d.option.Certificate.PrivateKey); err != nil {
 | 
			
		||||
		return fmt.Errorf("复制私钥失败: %w", err)
 | 
			
		||||
	case "pem":
 | 
			
		||||
		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 := 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))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 执行命令
 | 
			
		||||
	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" {
 | 
			
		||||
	switch d.option.DeployConfig.GetConfigAsString("shell") {
 | 
			
		||||
	case "cmd":
 | 
			
		||||
		cmd = exec.Command("cmd", "/C", command)
 | 
			
		||||
	} else {
 | 
			
		||||
 | 
			
		||||
	case "powershell":
 | 
			
		||||
		cmd = exec.Command("powershell", "-Command", command)
 | 
			
		||||
 | 
			
		||||
	case "sh":
 | 
			
		||||
		cmd = exec.Command("sh", "-c", 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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,10 +6,10 @@ 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"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -41,49 +41,84 @@ 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)
 | 
			
		||||
	if err := d.uploadFile(client, d.option.Certificate.Certificate, d.option.DeployConfig.GetConfigAsString("certPath")); err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to upload certificate file: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("ssh上传证书成功", 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)
 | 
			
		||||
	if err := d.uploadFile(client, d.option.Certificate.PrivateKey, d.option.DeployConfig.GetConfigAsString("keyPath")); err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to upload private key file: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d.infos = append(d.infos, toStr("ssh上传私钥成功", nil))
 | 
			
		||||
	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 +133,14 @@ 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) uploadFile(client *ssh.Client, path string, content string) 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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -122,33 +157,3 @@ func (d *SSHDeployer) upload(client *sshPkg.Client, content, path string) error
 | 
			
		|||
 | 
			
		||||
	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(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +17,7 @@ import DeployToTencentCOS from "./DeployToTencentCOS";
 | 
			
		|||
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";
 | 
			
		||||
| 
						 | 
				
			
			@ -136,8 +137,10 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
 | 
			
		|||
    case "qiniu-cdn":
 | 
			
		||||
      childComponent = <DeployToQiniuCDN />;
 | 
			
		||||
      break;
 | 
			
		||||
    case "ssh":
 | 
			
		||||
    case "local":
 | 
			
		||||
      childComponent = <DeployToLocal />;
 | 
			
		||||
      break;
 | 
			
		||||
    case "ssh":
 | 
			
		||||
      childComponent = <DeployToSSH />;
 | 
			
		||||
      break;
 | 
			
		||||
    case "webhook":
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,194 @@
 | 
			
		|||
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 { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
 | 
			
		||||
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: {
 | 
			
		||||
          certPath: "/etc/nginx/ssl/nginx.crt",
 | 
			
		||||
          keyPath: "/etc/nginx/ssl/nginx.key",
 | 
			
		||||
          shell: "sh",
 | 
			
		||||
          preCommand: "",
 | 
			
		||||
          command: "sudo service nginx reload",
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setError({});
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const formSchema = z.object({
 | 
			
		||||
    certPath: z.string().min(1, t("domain.deployment.form.file_cert_path.placeholder")),
 | 
			
		||||
    keyPath: z.string().min(1, t("domain.deployment.form.file_key_path.placeholder")),
 | 
			
		||||
    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(),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const res = formSchema.safeParse(data.config);
 | 
			
		||||
    if (!res.success) {
 | 
			
		||||
      setError({
 | 
			
		||||
        ...error,
 | 
			
		||||
        certPath: res.error.errors.find((e) => e.path[0] === "certPath")?.message,
 | 
			
		||||
        keyPath: res.error.errors.find((e) => e.path[0] === "keyPath")?.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,
 | 
			
		||||
        certPath: undefined,
 | 
			
		||||
        keyPath: undefined,
 | 
			
		||||
        shell: undefined,
 | 
			
		||||
        preCommand: undefined,
 | 
			
		||||
        command: undefined,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }, [data]);
 | 
			
		||||
 | 
			
		||||
  const getOptionCls = (val: string) => {
 | 
			
		||||
    if (data.config?.shell === val) {
 | 
			
		||||
      return "border-primary dark:border-primary";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return "";
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className="flex flex-col space-y-8">
 | 
			
		||||
        <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>
 | 
			
		||||
 | 
			
		||||
        <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>
 | 
			
		||||
 | 
			
		||||
        <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>
 | 
			
		||||
          <Label>{t("domain.deployment.form.shell_command.label")}</Label>
 | 
			
		||||
          <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,5 +1,6 @@
 | 
			
		|||
import { useEffect } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { z } from "zod";
 | 
			
		||||
import { produce } from "immer";
 | 
			
		||||
 | 
			
		||||
import { Input } from "@/components/ui/input";
 | 
			
		||||
| 
						 | 
				
			
			@ -9,13 +10,8 @@ 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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -31,79 +27,107 @@ const DeployToSSH = () => {
 | 
			
		|||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setError({});
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const formSchema = z.object({
 | 
			
		||||
    certPath: z.string().min(1, t("domain.deployment.form.file_cert_path.placeholder")),
 | 
			
		||||
    keyPath: z.string().min(1, t("domain.deployment.form.file_key_path.placeholder")),
 | 
			
		||||
    preCommand: z.string().optional(),
 | 
			
		||||
    command: z.string().optional(),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const res = formSchema.safeParse(data.config);
 | 
			
		||||
    if (!res.success) {
 | 
			
		||||
      setError({
 | 
			
		||||
        ...error,
 | 
			
		||||
        certPath: res.error.errors.find((e) => e.path[0] === "certPath")?.message,
 | 
			
		||||
        keyPath: res.error.errors.find((e) => e.path[0] === "keyPath")?.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,
 | 
			
		||||
        certPath: undefined,
 | 
			
		||||
        keyPath: undefined,
 | 
			
		||||
        preCommand: undefined,
 | 
			
		||||
        command: undefined,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }, [data]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className="flex flex-col space-y-8">
 | 
			
		||||
        <div>
 | 
			
		||||
          <Label>{t("domain.deployment.form.ssh_cert_path.label")}</Label>
 | 
			
		||||
          <Label>{t("domain.deployment.form.file_cert_path.label")}</Label>
 | 
			
		||||
          <Input
 | 
			
		||||
            placeholder={t("domain.deployment.form.ssh_cert_path.label")}
 | 
			
		||||
            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) => {
 | 
			
		||||
                if (!draft.config) {
 | 
			
		||||
                  draft.config = {};
 | 
			
		||||
                }
 | 
			
		||||
                draft.config.certPath = e.target.value;
 | 
			
		||||
                draft.config ??= {};
 | 
			
		||||
                draft.config.certPath = e.target.value?.trim();
 | 
			
		||||
              });
 | 
			
		||||
              setDeploy(newData);
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <div className="text-red-600 text-sm mt-1">{error?.certPath}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div>
 | 
			
		||||
          <Label>{t("domain.deployment.form.ssh_key_path.label")}</Label>
 | 
			
		||||
          <Label>{t("domain.deployment.form.file_key_path.label")}</Label>
 | 
			
		||||
          <Input
 | 
			
		||||
            placeholder={t("domain.deployment.form.ssh_key_path.placeholder")}
 | 
			
		||||
            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) => {
 | 
			
		||||
                if (!draft.config) {
 | 
			
		||||
                  draft.config = {};
 | 
			
		||||
                }
 | 
			
		||||
                draft.config.keyPath = e.target.value;
 | 
			
		||||
                draft.config ??= {};
 | 
			
		||||
                draft.config.keyPath = e.target.value?.trim();
 | 
			
		||||
              });
 | 
			
		||||
              setDeploy(newData);
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <div className="text-red-600 text-sm mt-1">{error?.keyPath}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div>
 | 
			
		||||
          <Label>{t("domain.deployment.form.ssh_pre_command.label")}</Label>
 | 
			
		||||
          <Label>{t("domain.deployment.form.shell_pre_command.label")}</Label>
 | 
			
		||||
          <Textarea
 | 
			
		||||
            className="mt-1"
 | 
			
		||||
            value={data?.config?.preCommand}
 | 
			
		||||
            placeholder={t("domain.deployment.form.ssh_pre_command.placeholder")}
 | 
			
		||||
            placeholder={t("domain.deployment.form.shell_pre_command.placeholder")}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              const newData = produce(data, (draft) => {
 | 
			
		||||
                if (!draft.config) {
 | 
			
		||||
                  draft.config = {};
 | 
			
		||||
                }
 | 
			
		||||
                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>
 | 
			
		||||
          <Label>{t("domain.deployment.form.ssh_command.label")}</Label>
 | 
			
		||||
          <Label>{t("domain.deployment.form.shell_command.label")}</Label>
 | 
			
		||||
          <Textarea
 | 
			
		||||
            className="mt-1"
 | 
			
		||||
            value={data?.config?.command}
 | 
			
		||||
            placeholder={t("domain.deployment.form.ssh_command.placeholder")}
 | 
			
		||||
            placeholder={t("domain.deployment.form.shell_command.placeholder")}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              const newData = produce(data, (draft) => {
 | 
			
		||||
                if (!draft.config) {
 | 
			
		||||
                  draft.config = {};
 | 
			
		||||
                }
 | 
			
		||||
                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>
 | 
			
		||||
    </>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -86,14 +86,19 @@
 | 
			
		|||
  "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_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.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.k8s_namespace.label": "Namespace",
 | 
			
		||||
  "domain.deployment.form.k8s_namespace.placeholder": "Please enter namespace",
 | 
			
		||||
  "domain.deployment.form.k8s_secret_name.label": "Secret Name",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -86,14 +86,16 @@
 | 
			
		|||
  "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_key_path.label": "私钥保存路径",
 | 
			
		||||
  "domain.deployment.form.file_key_path.placeholder": "请输入私钥保存路径",
 | 
			
		||||
  "domain.deployment.form.file_cert_path.label": "证书保存路径",
 | 
			
		||||
  "domain.deployment.form.file_cert_path.placeholder": "请输入证书保存路径",
 | 
			
		||||
  "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.k8s_namespace.label": "命名空间",
 | 
			
		||||
  "domain.deployment.form.k8s_namespace.placeholder": "请输入 K8S 命名空间",
 | 
			
		||||
  "domain.deployment.form.k8s_secret_name.label": "Secret 名称",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue