From 9f7cffce21a69b8d43371eb81541cba5abe4dc3b Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 23 Jan 2025 23:47:37 +0800 Subject: [PATCH] feat: allow fallback to use scp on deployment to ssh --- README.md | 2 +- README_EN.md | 2 +- go.mod | 1 + go.sum | 3 ++ internal/deployer/providers.go | 1 + .../pkg/core/deployer/providers/ssh/ssh.go | 53 ++++++++++++++++--- .../node/DeployNodeConfigFormSSHConfig.tsx | 13 ++++- .../i18n/locales/en/nls.workflow.nodes.json | 2 + .../i18n/locales/zh/nls.workflow.nodes.json | 2 + 9 files changed, 69 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 28438c76..97e851ea 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ make local.run | 提供商 | 备注 | | :-------------------------------------- | :------------------------------------------------------------------ | | 本地部署 | 可部署到本地服务器 | -| SSH 部署 | 可部署到远程服务器(通过 SSH+SFTP) | +| SSH 部署 | 可部署到远程服务器(通过 SSH+SFTP/SCP) | | Webhook 回调 | 可部署到 Webhook | | [Kubernetes](https://kubernetes.io/) | 可部署到 Kubernetes Secret | | [阿里云](https://www.aliyun.com/) | 可部署到阿里云 OSS、CDN、DCDN、SLB(CLB/ALB/NLB)、WAF、Live 等服务 | diff --git a/README_EN.md b/README_EN.md index 5b3e67ff..b5c71b3d 100644 --- a/README_EN.md +++ b/README_EN.md @@ -117,7 +117,7 @@ The following hosting providers are supported: | Provider | Remarks | | :---------------------------------------------- | :------------------------------------------------------------------------------- | | Local | Supports deployment to local servers | -| SSH | Supports deployment to remote servers (via SSH+SFTP) | +| SSH | Supports deployment to remote servers (via SSH+SFTP/SCP) | | Webhook | Supports deployment to Webhook | | [Kubernetes](https://kubernetes.io/) | Supports deployment to Kubernetes Secret | | [Alibaba Cloud](https://www.alibabacloud.com/) | Supports deployment to Alibaba Cloud OSS, CDN, DCDN, SLB(CLB/ALB/NLB), WAF, Live | diff --git a/go.mod b/go.mod index 3bb9f1d9..6cd082bf 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/pkg/sftp v1.13.7 github.com/pocketbase/dbx v1.11.0 github.com/pocketbase/pocketbase v0.24.4 + github.com/povsister/scp v0.0.0-20240802064259-28781e87b246 github.com/qiniu/go-sdk/v7 v7.25.2 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1084 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1084 diff --git a/go.sum b/go.sum index 2111474e..7dde5dc8 100644 --- a/go.sum +++ b/go.sum @@ -743,6 +743,8 @@ github.com/pocketbase/pocketbase v0.24.4 h1:kw/c23HccoxMV/19U9QlDcvNJgQ66vlUrxGQ github.com/pocketbase/pocketbase v0.24.4/go.mod h1:EfXV/8RUY76jA6g1RPNHjOuW7wTd2bz0QlvAI/RU8YY= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/povsister/scp v0.0.0-20240802064259-28781e87b246 h1:c4D8BPWLOxxdaxQLfLKQXH2YXY/E9yo3jrDSL54XrTw= +github.com/povsister/scp v0.0.0-20240802064259-28781e87b246/go.mod h1:i1Au86ZXK0ZalQNyBp2njCcyhSCR/QP/AMfILip+zNI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= @@ -933,6 +935,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 28e4108c..344c78e6 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -350,6 +350,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, SshPassword: access.Password, SshKey: access.Key, SshKeyPassphrase: access.KeyPassphrase, + UseSCP: maps.GetValueAsBool(options.ProviderDeployConfig, "useSCP"), PreCommand: maps.GetValueAsString(options.ProviderDeployConfig, "preCommand"), PostCommand: maps.GetValueAsString(options.ProviderDeployConfig, "postCommand"), OutputFormat: providerSSH.OutputFormatType(maps.GetValueOrDefaultAsString(options.ProviderDeployConfig, "format", string(providerSSH.OUTPUT_FORMAT_PEM))), diff --git a/internal/pkg/core/deployer/providers/ssh/ssh.go b/internal/pkg/core/deployer/providers/ssh/ssh.go index 4fffce74..20ea348a 100644 --- a/internal/pkg/core/deployer/providers/ssh/ssh.go +++ b/internal/pkg/core/deployer/providers/ssh/ssh.go @@ -10,6 +10,7 @@ import ( xerrors "github.com/pkg/errors" "github.com/pkg/sftp" + "github.com/povsister/scp" "golang.org/x/crypto/ssh" "github.com/usual2970/certimate/internal/pkg/core/deployer" @@ -32,6 +33,8 @@ type SshDeployerConfig struct { SshKey string `json:"sshKey,omitempty"` // SSH 登录私钥口令。 SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"` + // 是否回退使用 SCP。 + UseSCP bool `json:"useSCP,omitempty"` // 前置命令。 PreCommand string `json:"preCommand,omitempty"` // 后置命令。 @@ -112,13 +115,13 @@ func (d *SshDeployer) Deploy(ctx context.Context, certPem string, privkeyPem str // 上传证书和私钥文件 switch d.config.OutputFormat { case OUTPUT_FORMAT_PEM: - if err := writeSftpFileString(client, d.config.OutputCertPath, certPem); err != nil { + if err := writeFileString(client, d.config.UseSCP, d.config.OutputCertPath, certPem); err != nil { return nil, xerrors.Wrap(err, "failed to upload certificate file") } d.logger.Logt("certificate file uploaded") - if err := writeSftpFileString(client, d.config.OutputKeyPath, privkeyPem); err != nil { + if err := writeFileString(client, d.config.UseSCP, d.config.OutputKeyPath, privkeyPem); err != nil { return nil, xerrors.Wrap(err, "failed to upload private key file") } @@ -132,7 +135,7 @@ func (d *SshDeployer) Deploy(ctx context.Context, certPem string, privkeyPem str d.logger.Logt("certificate transformed to PFX") - if err := writeSftpFile(client, d.config.OutputCertPath, pfxData); err != nil { + if err := writeFile(client, d.config.UseSCP, d.config.OutputCertPath, pfxData); err != nil { return nil, xerrors.Wrap(err, "failed to upload certificate file") } @@ -146,7 +149,7 @@ func (d *SshDeployer) Deploy(ctx context.Context, certPem string, privkeyPem str d.logger.Logt("certificate transformed to JKS") - if err := writeSftpFile(client, d.config.OutputCertPath, jksData); err != nil { + if err := writeFile(client, d.config.UseSCP, d.config.OutputCertPath, jksData); err != nil { return nil, xerrors.Wrap(err, "failed to upload certificate file") } @@ -223,11 +226,47 @@ func execSshCommand(sshCli *ssh.Client, command string) (string, string, error) return stdoutBuf.String(), stderrBuf.String(), nil } -func writeSftpFileString(sshCli *ssh.Client, path string, content string) error { - return writeSftpFile(sshCli, path, []byte(content)) +func writeFileString(sshCli *ssh.Client, useSCP bool, path string, content string) error { + if useSCP { + return writeFileStringWithSCP(sshCli, path, content) + } + + return writeFileStringWithSFTP(sshCli, path, content) } -func writeSftpFile(sshCli *ssh.Client, path string, data []byte) error { +func writeFile(sshCli *ssh.Client, useSCP bool, path string, data []byte) error { + if useSCP { + return writeFileWithSCP(sshCli, path, data) + } + + return writeFileWithSFTP(sshCli, path, data) +} + +func writeFileStringWithSCP(sshCli *ssh.Client, path string, content string) error { + return writeFileWithSCP(sshCli, path, []byte(content)) +} + +func writeFileWithSCP(sshCli *ssh.Client, path string, data []byte) error { + scpCli, err := scp.NewClientFromExistingSSH(sshCli, &scp.ClientOption{}) + if err != nil { + return xerrors.Wrap(err, "failed to create scp client") + } + defer scpCli.Close() + + reader := bytes.NewReader(data) + err = scpCli.CopyToRemote(reader, path, &scp.FileTransferOption{}) + if err != nil { + return xerrors.Wrap(err, "failed to write to remote file") + } + + return nil +} + +func writeFileStringWithSFTP(sshCli *ssh.Client, path string, content string) error { + return writeFileWithSFTP(sshCli, path, []byte(content)) +} + +func writeFileWithSFTP(sshCli *ssh.Client, path string, data []byte) error { sftpCli, err := sftp.NewClient(sshCli) if err != nil { return xerrors.Wrap(err, "failed to create sftp client") diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx index 976ca079..1e176d7b 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; import { DownOutlined as DownOutlinedIcon } from "@ant-design/icons"; -import { Button, Dropdown, Form, type FormInstance, Input, Select } from "antd"; +import { Button, Dropdown, Form, type FormInstance, Input, Select, Switch } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; @@ -17,6 +17,7 @@ type DeployNodeConfigFormSSHConfigFieldValues = Nullish<{ jksStorepass?: string | null; preCommand?: string | null; postCommand?: string | null; + useSCP?: boolean; }>; export type DeployNodeConfigFormSSHConfigProps = { @@ -89,6 +90,7 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini .string() .max(20480, t("common.errmsg.string_max", { max: 20480 })) .nullish(), + useSCP: z.boolean().nullish(), }); const formRule = createSchemaFieldRule(formSchema); @@ -261,6 +263,15 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini + + } + > + + ); }; diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 7ba7e643..964633e0 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -291,6 +291,8 @@ "workflow_node.deploy.form.ssh_post_command.placeholder": "Please enter command to be executed after uploading files", "workflow_node.deploy.form.ssh_preset_scripts.button": "Use preset scripts", "workflow_node.deploy.form.ssh_preset_scripts.option.reload_nginx.label": "POSIX Bash - Reload nginx", + "workflow_node.deploy.form.ssh_use_scp.label": "Fallback to use SCP", + "workflow_node.deploy.form.ssh_use_scp.tooltip": "If the remote server does not support SFTP, please enable this option to fallback to SCP.", "workflow_node.deploy.form.tencentcloud_cdn_domain.label": "Tencent Cloud CDN domain", "workflow_node.deploy.form.tencentcloud_cdn_domain.placeholder": "Please enter Tencent Cloud CDN domain name", "workflow_node.deploy.form.tencentcloud_cdn_domain.tooltip": "For more information, see https://console.tencentcloud.com/cdn", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 147bb63a..d132f935 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -291,6 +291,8 @@ "workflow_node.deploy.form.ssh_post_command.placeholder": "请输入保存文件后执行的命令", "workflow_node.deploy.form.ssh_preset_scripts.button": "使用预设脚本", "workflow_node.deploy.form.ssh_preset_scripts.option.reload_nginx.label": "POSIX Bash - 重启 nginx 进程", + "workflow_node.deploy.form.ssh_use_scp.label": "回退使用 SCP", + "workflow_node.deploy.form.ssh_use_scp.tooltip": "如果你的远程服务器不支持 SFTP,请开启此选项回退为 SCP。", "workflow_node.deploy.form.tencentcloud_cdn_domain.label": "腾讯云 CDN 加速域名(支持泛域名)", "workflow_node.deploy.form.tencentcloud_cdn_domain.placeholder": "请输入腾讯云 CDN 加速域名", "workflow_node.deploy.form.tencentcloud_cdn_domain.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/cdn

泛域名表示形式为:*.example.com",