feat(provider): add support for REPL mode on deployment to SSH

This commit is contained in:
gdyr
2026-09-14 16:44:19 +08:00
committed by GitHub
parent f561d6e338
commit 80a2b60a5f
8 changed files with 95 additions and 54 deletions
+1
View File
@@ -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"`
+14 -26
View File
@@ -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)
+22 -23
View File
@@ -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
+34
View File
@@ -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 或更高版本。"
},