mirror of
https://github.com/certimate-go/certimate.git
synced 2026-10-16 15:16:05 +08:00
feat(provider): add support for REPL mode on deployment to SSH
This commit is contained in:
@@ -41,6 +41,7 @@ func init() {
|
||||
},
|
||||
JumpServers: jumpServers,
|
||||
UseSCP: xmaps.GetBool(options.ProviderExtendedConfig, "useSCP"),
|
||||
UseREPL: xmaps.GetBool(options.ProviderExtendedConfig, "useREPL"),
|
||||
PreCommand: xmaps.GetString(options.ProviderExtendedConfig, "preCommand"),
|
||||
PostCommand: xmaps.GetString(options.ProviderExtendedConfig, "postCommand"),
|
||||
FileFormat: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "fileFormat", dplyimpl.FILE_FORMAT_PEM),
|
||||
|
||||
@@ -34,7 +34,7 @@ type ChallengerConfig struct {
|
||||
|
||||
// 跳板机配置数组。
|
||||
JumpServers []ServerConfig `json:"jumpServers,omitempty"`
|
||||
// 是否回退使用 SCP。
|
||||
// 是否使用 SCP 传输文件。
|
||||
UseSCP bool `json:"useSCP,omitempty"`
|
||||
// 网站根目录路径。
|
||||
WebRootPath string `json:"webRootPath"`
|
||||
|
||||
@@ -92,17 +92,7 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
|
||||
|
||||
// 执行前置命令
|
||||
if d.config.PreCommand != "" {
|
||||
command := d.config.PreCommand
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}", d.config.FilePathForCrt)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}", d.config.FilePathForCrtOnlyServer)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}", d.config.FilePathForCrtOnlyIntermedia)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}", d.config.FilePathForKey)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}", d.config.PfxPassword)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}", d.config.JksAlias)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}", d.config.JksKeypass)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}", d.config.JksStorepass)
|
||||
|
||||
stdout, stderr, err := execCommand(d.config.ShellEnv, command)
|
||||
stdout, stderr, err := execCommand(d.config.PreCommand, *d.config)
|
||||
d.logger.Debug("run pre-command", slog.String("stdout", stdout), slog.String("stderr", stderr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute pre-command (stdout: %s, stderr: %s): %w ", stdout, stderr, err)
|
||||
@@ -199,17 +189,7 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
|
||||
|
||||
// 执行后置命令
|
||||
if d.config.PostCommand != "" {
|
||||
command := d.config.PostCommand
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}", d.config.FilePathForCrt)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}", d.config.FilePathForCrtOnlyServer)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}", d.config.FilePathForCrtOnlyIntermedia)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}", d.config.FilePathForKey)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}", d.config.PfxPassword)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}", d.config.JksAlias)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}", d.config.JksKeypass)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}", d.config.JksStorepass)
|
||||
|
||||
stdout, stderr, err := execCommand(d.config.ShellEnv, command)
|
||||
stdout, stderr, err := execCommand(d.config.PostCommand, *d.config)
|
||||
d.logger.Debug("run post-command", slog.String("stdout", stdout), slog.String("stderr", stderr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute post-command (stdout: %s, stderr: %s): %w ", stdout, stderr, err)
|
||||
@@ -219,10 +199,18 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
|
||||
return &DeployResult{}, nil
|
||||
}
|
||||
|
||||
func execCommand(shellEnv string, command string) (string, string, error) {
|
||||
var cmd *exec.Cmd
|
||||
func execCommand(command string, config DeployerConfig) (string, string, error) {
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}", config.FilePathForCrt)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}", config.FilePathForCrtOnlyServer)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}", config.FilePathForCrtOnlyIntermedia)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}", config.FilePathForKey)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}", config.PfxPassword)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}", config.JksAlias)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}", config.JksKeypass)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}", config.JksStorepass)
|
||||
|
||||
switch shellEnv {
|
||||
var cmd *exec.Cmd
|
||||
switch config.ShellEnv {
|
||||
case "":
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = exec.Command("cmd", "/C", command)
|
||||
@@ -240,7 +228,7 @@ func execCommand(shellEnv string, command string) (string, string, error) {
|
||||
cmd = exec.Command("powershell", "-Command", command)
|
||||
|
||||
default:
|
||||
return "", "", fmt.Errorf("unsupported shell env '%s'", shellEnv)
|
||||
return "", "", fmt.Errorf("unsupported shell env '%s'", config.ShellEnv)
|
||||
}
|
||||
|
||||
stdoutBuf := bytes.NewBuffer(nil)
|
||||
|
||||
@@ -44,8 +44,10 @@ type DeployerConfig struct {
|
||||
|
||||
// 跳板机配置数组。
|
||||
JumpServers []ServerConfig `json:"jumpServers,omitempty"`
|
||||
// 是否回退使用 SCP。
|
||||
// 是否使用 SCP 传输文件。
|
||||
UseSCP bool `json:"useSCP,omitempty"`
|
||||
// 是否使用 REPL 命令行。
|
||||
UseREPL bool `json:"useREPL,omitempty"`
|
||||
// 前置命令。
|
||||
PreCommand string `json:"preCommand,omitempty"`
|
||||
// 后置命令。
|
||||
@@ -122,17 +124,7 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
|
||||
|
||||
// 执行前置命令
|
||||
if d.config.PreCommand != "" {
|
||||
command := d.config.PreCommand
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}", d.config.FilePathForCrt)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}", d.config.FilePathForCrtOnlyServer)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}", d.config.FilePathForCrtOnlyIntermedia)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}", d.config.FilePathForKey)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}", d.config.PfxPassword)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}", d.config.JksAlias)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}", d.config.JksKeypass)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}", d.config.JksStorepass)
|
||||
|
||||
stdout, stderr, err := xssh.RunCommand(sshClient.RawClient(), command)
|
||||
stdout, stderr, err := execCommand(sshClient, d.config.PreCommand, *d.config)
|
||||
d.logger.Debug("run pre-command", slog.String("stdout", stdout), slog.String("stderr", stderr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute pre-command (stdout: %s, stderr: %s): %w ", stdout, stderr, err)
|
||||
@@ -229,17 +221,7 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
|
||||
|
||||
// 执行后置命令
|
||||
if d.config.PostCommand != "" {
|
||||
command := d.config.PostCommand
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}", d.config.FilePathForCrt)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}", d.config.FilePathForCrtOnlyServer)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}", d.config.FilePathForCrtOnlyIntermedia)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}", d.config.FilePathForKey)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}", d.config.PfxPassword)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}", d.config.JksAlias)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}", d.config.JksKeypass)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}", d.config.JksStorepass)
|
||||
|
||||
stdout, stderr, err := xssh.RunCommand(sshClient.RawClient(), command)
|
||||
stdout, stderr, err := execCommand(sshClient, d.config.PostCommand, *d.config)
|
||||
d.logger.Debug("run post-command", slog.String("stdout", stdout), slog.String("stderr", stderr))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute post-command (stdout: %s, stderr: %s): %w ", stdout, stderr, err)
|
||||
@@ -249,6 +231,23 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
|
||||
return &DeployResult{}, nil
|
||||
}
|
||||
|
||||
func execCommand(sshClient *ssh.Client, command string, config DeployerConfig) (string, string, error) {
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}", config.FilePathForCrt)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}", config.FilePathForCrtOnlyServer)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}", config.FilePathForCrtOnlyIntermedia)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}", config.FilePathForKey)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}", config.PfxPassword)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}", config.JksAlias)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}", config.JksKeypass)
|
||||
command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}", config.JksStorepass)
|
||||
|
||||
if config.UseREPL {
|
||||
return xssh.RunCommandWithREPL(sshClient.RawClient(), command)
|
||||
}
|
||||
|
||||
return xssh.RunCommand(sshClient.RawClient(), command)
|
||||
}
|
||||
|
||||
func createSshClient(config DeployerConfig) (*ssh.Client, error) {
|
||||
clientCfg := ssh.NewDefaultConfig()
|
||||
clientCfg.Host = config.SshHost
|
||||
|
||||
@@ -3,6 +3,7 @@ package ssh
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
@@ -35,3 +36,36 @@ func RunCommand(sshCli *ssh.Client, command string) (string, string, error) {
|
||||
|
||||
return stdoutBuf.String(), stderrBuf.String(), nil
|
||||
}
|
||||
|
||||
// 与 [RunCommand] 类似,但以 REPL 模式逐行执行脚本。
|
||||
//
|
||||
// 入参:
|
||||
// - sshCli: SSH 客户端。
|
||||
// - commands: 待执行的脚本命令。
|
||||
//
|
||||
// 出参:
|
||||
// - stdout:标准输出。
|
||||
// - stderr:标准错误。
|
||||
// - err: 错误。
|
||||
func RunCommandWithREPL(sshCli *ssh.Client, command string) (string, string, error) {
|
||||
lines := make([]string, 0)
|
||||
for _, line := range strings.Split(command, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
|
||||
stdouts := make([]string, 0, len(lines))
|
||||
stderrs := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
stdout, stderr, err := RunCommand(sshCli, line)
|
||||
stdouts = append(stdouts, stdout)
|
||||
stderrs = append(stderrs, stderr)
|
||||
if err != nil {
|
||||
return strings.Join(stdouts, "\n"), strings.Join(stderrs, "\n"), err
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(stdouts, "\n"), strings.Join(stderrs, "\n"), nil
|
||||
}
|
||||
|
||||
@@ -467,6 +467,16 @@ const BizDeployNodeConfigFieldsProviderSSH = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[parentNamePath, "useREPL"]}
|
||||
initialValue={initialValues.useREPL}
|
||||
label={t("workflow_node.deploy.form.ssh_use_repl.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.ssh_use_repl.tooltip") }}></span>}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -497,6 +507,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
|
||||
jksStorepass: z.string().nullish(),
|
||||
preCommand: z.string().max(20480).nullish(),
|
||||
postCommand: z.string().max(20480).nullish(),
|
||||
useREPL: z.boolean().nullish(),
|
||||
})
|
||||
.superRefine((values, ctx) => {
|
||||
switch (values.fileFormat) {
|
||||
|
||||
@@ -342,7 +342,7 @@
|
||||
"tooltip": "It's the main directory where the website's files are stored on the server."
|
||||
},
|
||||
"ssh_use_scp": {
|
||||
"label": "Fallback to use SCP",
|
||||
"label": "Use SCP to transfer file",
|
||||
"tooltip": "If the remote server does not support SFTP, please check this option to fallback to SCP."
|
||||
},
|
||||
"ucloud_udnr_endpoint": {
|
||||
@@ -2220,9 +2220,13 @@
|
||||
"ps_binding_rdp": "PowerShell - Binding RDP"
|
||||
},
|
||||
"ssh_use_scp": {
|
||||
"label": "Fallback to use SCP",
|
||||
"label": "Use SCP to transfer file",
|
||||
"tooltip": "If the remote server does not support SFTP, please check this option to fallback to SCP."
|
||||
},
|
||||
"ssh_use_repl": {
|
||||
"label": "Use REPL mode to execute commands (Optional)",
|
||||
"tooltip": "If the remote server has no shell and does not support multi-line commands, please check this option to enable REPL mode for line-by-line command execution."
|
||||
},
|
||||
"synologydsm": {
|
||||
"guide": "Requires Synology DSM v6.0 or higher."
|
||||
},
|
||||
|
||||
@@ -341,7 +341,7 @@
|
||||
"tooltip": "即服务器上存储网站文件的主文件夹。"
|
||||
},
|
||||
"ssh_use_scp": {
|
||||
"label": "回退使用 SCP",
|
||||
"label": "使用 SCP 传输文件",
|
||||
"tooltip": "如果你的远程服务器不支持 SFTP,请勾选此选项回退为 SCP。"
|
||||
},
|
||||
"ucloud_udnr_endpoint": {
|
||||
@@ -2219,9 +2219,13 @@
|
||||
"ps_binding_rdp": "PowerShell - 导入并绑定到 RDP"
|
||||
},
|
||||
"ssh_use_scp": {
|
||||
"label": "回退使用 SCP",
|
||||
"label": "使用 SCP 传输文件",
|
||||
"tooltip": "如果你的远程服务器不支持 SFTP,请勾选此选项回退为 SCP。"
|
||||
},
|
||||
"ssh_use_repl": {
|
||||
"label": "使用 REPL 模式执行命令(可选)",
|
||||
"tooltip": "如果你的远程服务器未提供 Shell、不支持多行命令,请勾选此选项启用 REPL 模式以逐行执行命令。"
|
||||
},
|
||||
"synologydsm": {
|
||||
"guide": "需要群晖 DSM v6.0 或更高版本。"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user