feat: allow fallback to use scp on deployment to ssh

This commit is contained in:
Fu Diwei 2025-01-23 23:47:37 +08:00
parent 5ee5460612
commit 9f7cffce21
9 changed files with 69 additions and 10 deletions

View File

@ -118,7 +118,7 @@ make local.run
| 提供商 | 备注 | | 提供商 | 备注 |
| :-------------------------------------- | :------------------------------------------------------------------ | | :-------------------------------------- | :------------------------------------------------------------------ |
| 本地部署 | 可部署到本地服务器 | | 本地部署 | 可部署到本地服务器 |
| SSH 部署 | 可部署到远程服务器(通过 SSH+SFTP | | SSH 部署 | 可部署到远程服务器(通过 SSH+SFTP/SCP |
| Webhook 回调 | 可部署到 Webhook | | Webhook 回调 | 可部署到 Webhook |
| [Kubernetes](https://kubernetes.io/) | 可部署到 Kubernetes Secret | | [Kubernetes](https://kubernetes.io/) | 可部署到 Kubernetes Secret |
| [阿里云](https://www.aliyun.com/) | 可部署到阿里云 OSS、CDN、DCDN、SLBCLB/ALB/NLB、WAF、Live 等服务 | | [阿里云](https://www.aliyun.com/) | 可部署到阿里云 OSS、CDN、DCDN、SLBCLB/ALB/NLB、WAF、Live 等服务 |

View File

@ -117,7 +117,7 @@ The following hosting providers are supported:
| Provider | Remarks | | Provider | Remarks |
| :---------------------------------------------- | :------------------------------------------------------------------------------- | | :---------------------------------------------- | :------------------------------------------------------------------------------- |
| Local | Supports deployment to local servers | | 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 | | Webhook | Supports deployment to Webhook |
| [Kubernetes](https://kubernetes.io/) | Supports deployment to Kubernetes Secret | | [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 | | [Alibaba Cloud](https://www.alibabacloud.com/) | Supports deployment to Alibaba Cloud OSS, CDN, DCDN, SLB(CLB/ALB/NLB), WAF, Live |

1
go.mod
View File

@ -29,6 +29,7 @@ require (
github.com/pkg/sftp v1.13.7 github.com/pkg/sftp v1.13.7
github.com/pocketbase/dbx v1.11.0 github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/pocketbase v0.24.4 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/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/cdn v1.0.1084
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1084 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1084

3
go.sum
View File

@ -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/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.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= 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 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.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 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-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-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-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-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-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=

View File

@ -350,6 +350,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
SshPassword: access.Password, SshPassword: access.Password,
SshKey: access.Key, SshKey: access.Key,
SshKeyPassphrase: access.KeyPassphrase, SshKeyPassphrase: access.KeyPassphrase,
UseSCP: maps.GetValueAsBool(options.ProviderDeployConfig, "useSCP"),
PreCommand: maps.GetValueAsString(options.ProviderDeployConfig, "preCommand"), PreCommand: maps.GetValueAsString(options.ProviderDeployConfig, "preCommand"),
PostCommand: maps.GetValueAsString(options.ProviderDeployConfig, "postCommand"), PostCommand: maps.GetValueAsString(options.ProviderDeployConfig, "postCommand"),
OutputFormat: providerSSH.OutputFormatType(maps.GetValueOrDefaultAsString(options.ProviderDeployConfig, "format", string(providerSSH.OUTPUT_FORMAT_PEM))), OutputFormat: providerSSH.OutputFormatType(maps.GetValueOrDefaultAsString(options.ProviderDeployConfig, "format", string(providerSSH.OUTPUT_FORMAT_PEM))),

View File

@ -10,6 +10,7 @@ import (
xerrors "github.com/pkg/errors" xerrors "github.com/pkg/errors"
"github.com/pkg/sftp" "github.com/pkg/sftp"
"github.com/povsister/scp"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/deployer"
@ -32,6 +33,8 @@ type SshDeployerConfig struct {
SshKey string `json:"sshKey,omitempty"` SshKey string `json:"sshKey,omitempty"`
// SSH 登录私钥口令。 // SSH 登录私钥口令。
SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"` SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"`
// 是否回退使用 SCP。
UseSCP bool `json:"useSCP,omitempty"`
// 前置命令。 // 前置命令。
PreCommand string `json:"preCommand,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 { switch d.config.OutputFormat {
case OUTPUT_FORMAT_PEM: 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") return nil, xerrors.Wrap(err, "failed to upload certificate file")
} }
d.logger.Logt("certificate file uploaded") 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") 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") 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") 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") 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") 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 return stdoutBuf.String(), stderrBuf.String(), nil
} }
func writeSftpFileString(sshCli *ssh.Client, path string, content string) error { func writeFileString(sshCli *ssh.Client, useSCP bool, path string, content string) error {
return writeSftpFile(sshCli, path, []byte(content)) 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) sftpCli, err := sftp.NewClient(sshCli)
if err != nil { if err != nil {
return xerrors.Wrap(err, "failed to create sftp client") return xerrors.Wrap(err, "failed to create sftp client")

View File

@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DownOutlined as DownOutlinedIcon } from "@ant-design/icons"; 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 { createSchemaFieldRule } from "antd-zod";
import { z } from "zod"; import { z } from "zod";
@ -17,6 +17,7 @@ type DeployNodeConfigFormSSHConfigFieldValues = Nullish<{
jksStorepass?: string | null; jksStorepass?: string | null;
preCommand?: string | null; preCommand?: string | null;
postCommand?: string | null; postCommand?: string | null;
useSCP?: boolean;
}>; }>;
export type DeployNodeConfigFormSSHConfigProps = { export type DeployNodeConfigFormSSHConfigProps = {
@ -89,6 +90,7 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
.string() .string()
.max(20480, t("common.errmsg.string_max", { max: 20480 })) .max(20480, t("common.errmsg.string_max", { max: 20480 }))
.nullish(), .nullish(),
useSCP: z.boolean().nullish(),
}); });
const formRule = createSchemaFieldRule(formSchema); const formRule = createSchemaFieldRule(formSchema);
@ -261,6 +263,15 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.ssh_post_command.placeholder")} /> <Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.ssh_post_command.placeholder")} />
</Form.Item> </Form.Item>
</Form.Item> </Form.Item>
<Form.Item
name="useSCP"
label={t("workflow_node.deploy.form.ssh_use_scp.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.ssh_use_scp.tooltip") }}></span>}
>
<Switch />
</Form.Item>
</Form> </Form>
); );
}; };

View File

@ -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_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.button": "Use preset scripts",
"workflow_node.deploy.form.ssh_preset_scripts.option.reload_nginx.label": "POSIX Bash - Reload nginx", "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.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.placeholder": "Please enter Tencent Cloud CDN domain name",
"workflow_node.deploy.form.tencentcloud_cdn_domain.tooltip": "For more information, see <a href=\"https://console.tencentcloud.com/cdn\" target=\"_blank\">https://console.tencentcloud.com/cdn</a>", "workflow_node.deploy.form.tencentcloud_cdn_domain.tooltip": "For more information, see <a href=\"https://console.tencentcloud.com/cdn\" target=\"_blank\">https://console.tencentcloud.com/cdn</a>",

View File

@ -291,6 +291,8 @@
"workflow_node.deploy.form.ssh_post_command.placeholder": "请输入保存文件后执行的命令", "workflow_node.deploy.form.ssh_post_command.placeholder": "请输入保存文件后执行的命令",
"workflow_node.deploy.form.ssh_preset_scripts.button": "使用预设脚本", "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_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.label": "腾讯云 CDN 加速域名(支持泛域名)",
"workflow_node.deploy.form.tencentcloud_cdn_domain.placeholder": "请输入腾讯云 CDN 加速域名", "workflow_node.deploy.form.tencentcloud_cdn_domain.placeholder": "请输入腾讯云 CDN 加速域名",
"workflow_node.deploy.form.tencentcloud_cdn_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/cdn\" target=\"_blank\">https://console.cloud.tencent.com/cdn</a><br><br>泛域名表示形式为:*.example.com", "workflow_node.deploy.form.tencentcloud_cdn_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/cdn\" target=\"_blank\">https://console.cloud.tencent.com/cdn</a><br><br>泛域名表示形式为:*.example.com",