From bcd8f8dea1812cdeae0d0d778c6ad4153d5c2ec7 Mon Sep 17 00:00:00 2001 From: yoan <536464346@qq.com> Date: Thu, 22 Aug 2024 20:24:00 +0800 Subject: [PATCH] support ssh deployemnt --- .gitignore | 2 +- go.mod | 12 +- go.sum | 28 +- internal/deployer/deployer.go | 6 + internal/deployer/ssh.go | 125 +++++ internal/domains/deploy.go | 8 +- internal/domains/history.go | 4 +- migrations/1724329413_collections_snapshot.go | 501 ++++++++++++++++++ ui/public/imgs/providers/ssh.png | Bin 0 -> 1002 bytes ui/src/components/certimate/AccessEdit.tsx | 12 + ui/src/components/certimate/AccessSSHForm.tsx | 348 ++++++++++++ ui/src/domain/access.ts | 17 +- ui/src/domain/domain.ts | 1 + ui/src/lib/file.ts | 17 + ui/src/pages/domains/Home.tsx | 2 +- 15 files changed, 1060 insertions(+), 23 deletions(-) create mode 100644 internal/deployer/ssh.go create mode 100644 migrations/1724329413_collections_snapshot.go create mode 100644 ui/public/imgs/providers/ssh.png create mode 100644 ui/src/components/certimate/AccessSSHForm.tsx create mode 100644 ui/src/lib/file.ts diff --git a/.gitignore b/.gitignore index 81dfbea9..da0ee1a3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ __debug_bin* vendor pb_data main -certimate +./certimate build # Editor directories and files diff --git a/go.mod b/go.mod index 2e1f87e0..55e91025 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,10 @@ require ( github.com/alibabacloud-go/tea-utils/v2 v2.0.5 github.com/go-acme/lego/v4 v4.17.4 github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 + github.com/pkg/sftp v1.13.6 github.com/pocketbase/dbx v1.10.1 github.com/pocketbase/pocketbase v0.22.18 + golang.org/x/crypto v0.26.0 ) require ( @@ -66,6 +68,7 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/kr/fs v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect @@ -86,15 +89,14 @@ require ( github.com/valyala/fasttemplate v1.2.2 // indirect go.opencensus.io v0.24.0 // indirect gocloud.dev v0.37.0 // indirect - golang.org/x/crypto v0.25.0 // indirect golang.org/x/image v0.18.0 // indirect golang.org/x/mod v0.18.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.22.0 // indirect golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect diff --git a/go.sum b/go.sum index 6d4f4de5..0e35d96d 100644 --- a/go.sum +++ b/go.sum @@ -213,6 +213,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -249,6 +251,8 @@ github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:Ff github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= +github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pocketbase/dbx v1.10.1 h1:cw+vsyfCJD8YObOVeqb93YErnlxwYMkNZ4rwN0G0AaA= @@ -324,10 +328,11 @@ golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -362,6 +367,7 @@ golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= @@ -379,8 +385,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -394,21 +400,23 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -418,8 +426,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index c12fbc75..cff4386f 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -16,6 +16,7 @@ const ( const ( targetAliyunOss = "aliyun-oss" targetAliyunCdn = "aliyun-cdn" + targetSSH = "ssh" ) type DeployerOption struct { @@ -47,6 +48,8 @@ func Get(record *models.Record) (Deployer, error) { return NewAliyun(option) case targetAliyunCdn: return NewAliyunCdn(option) + case targetSSH: + return NewSSH(option) } return nil, errors.New("not implemented") } @@ -54,5 +57,8 @@ func Get(record *models.Record) (Deployer, error) { func getProduct(record *models.Record) string { targetType := record.GetString("targetType") rs := strings.Split(targetType, "-") + if len(rs) < 2 { + return "" + } return rs[1] } diff --git a/internal/deployer/ssh.go b/internal/deployer/ssh.go new file mode 100644 index 00000000..bd844c43 --- /dev/null +++ b/internal/deployer/ssh.go @@ -0,0 +1,125 @@ +package deployer + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + xpath "path" + + "github.com/pkg/sftp" + sshPkg "golang.org/x/crypto/ssh" +) + +type ssh struct { + option *DeployerOption +} + +type sshAccess struct { + Host string `json:"host"` + Username string `json:"username"` + Password string `json:"password"` + Key string `json:"key"` + Port string `json:"port"` + Command string `json:"command"` + CertPath string `json:"certPath"` + KeyPath string `json:"keyPath"` +} + +func NewSSH(option *DeployerOption) (Deployer, error) { + return &ssh{ + option: option, + }, nil +} + +func (s *ssh) Deploy(ctx context.Context) error { + access := &sshAccess{} + if err := json.Unmarshal([]byte(s.option.Access), access); err != nil { + return err + } + // 连接 + client, err := s.getClient(access) + if err != nil { + return err + } + defer client.Close() + + // 上传 + session, err := client.NewSession() + if err != nil { + return fmt.Errorf("failed to create session: %w", err) + } + defer session.Close() + + // 上传证书 + if err := s.upload(client, s.option.Certificate.Certificate, access.CertPath); err != nil { + return fmt.Errorf("failed to upload certificate: %w", err) + } + + // 上传私钥 + if err := s.upload(client, s.option.Certificate.PrivateKey, access.KeyPath); err != nil { + return fmt.Errorf("failed to upload private key: %w", err) + } + + // 执行命令 + var stdoutBuf bytes.Buffer + session.Stdout = &stdoutBuf + var stderrBuf bytes.Buffer + session.Stderr = &stderrBuf + + if err := session.Run(access.Command); err != nil { + return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdoutBuf.String(), stderrBuf.String()) + } + + return nil +} + +func (s *ssh) upload(client *sshPkg.Client, content, path 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.Base(path)); err != nil { + return fmt.Errorf("failed to create remote directory: %w", err) + } + + file, err := sftpCli.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC) + if err != nil { + return fmt.Errorf("failed to open remote file: %w", err) + } + defer file.Close() + + _, err = file.Write([]byte(content)) + if err != nil { + return fmt.Errorf("failed to write to remote file: %w", err) + } + + return nil +} + +func (s *ssh) getClient(access *sshAccess) (*sshPkg.Client, error) { + + var authMethod sshPkg.AuthMethod + + if access.Key != "" { + 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(), + }) +} diff --git a/internal/domains/deploy.go b/internal/domains/deploy.go index 5a3c21e0..429817e2 100644 --- a/internal/domains/deploy.go +++ b/internal/domains/deploy.go @@ -21,7 +21,11 @@ const ( ) func deploy(ctx context.Context, record *models.Record) error { - + defer func() { + if r := recover(); r != nil { + app.GetApp().Logger().Error("部署失败", "err", r) + } + }() currRecord, err := app.GetApp().Dao().FindRecordById("domains", record.Id) history := NewHistory(record) defer history.commit() @@ -86,7 +90,7 @@ func deploy(ctx context.Context, record *models.Record) error { history.record(applyPhase, "保存证书成功", nil, true) // ############3.部署证书 - history.record(deployPhase, "开始部署", nil) + history.record(deployPhase, "开始部署", nil, false) deployer, err := deployer.Get(currRecord) if err != nil { history.record(deployPhase, "获取deployer失败", err) diff --git a/internal/domains/history.go b/internal/domains/history.go index 6823778c..344c5933 100644 --- a/internal/domains/history.go +++ b/internal/domains/history.go @@ -34,8 +34,8 @@ func NewHistory(record *models.Record) *history { func (a *history) record(phase Phase, msg string, err error, pass ...bool) { a.Phase = phase - if len(pass) > 0 && pass[0] { - a.PhaseSuccess = true + if len(pass) > 0 { + a.PhaseSuccess = pass[0] } errMsg := "" diff --git a/migrations/1724329413_collections_snapshot.go b/migrations/1724329413_collections_snapshot.go new file mode 100644 index 00000000..c6d23a4f --- /dev/null +++ b/migrations/1724329413_collections_snapshot.go @@ -0,0 +1,501 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models" +) + +func init() { + m.Register(func(db dbx.Builder) error { + jsonData := `[ + { + "id": "_pb_users_auth_", + "created": "2024-07-29 09:44:56.398Z", + "updated": "2024-08-21 04:13:40.056Z", + "name": "users", + "type": "auth", + "system": false, + "schema": [ + { + "system": false, + "id": "users_name", + "name": "name", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "users_avatar", + "name": "avatar", + "type": "file", + "required": false, + "presentable": false, + "unique": false, + "options": { + "mimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/gif", + "image/webp" + ], + "thumbs": null, + "maxSelect": 1, + "maxSize": 5242880, + "protected": false + } + } + ], + "indexes": [], + "listRule": "id = @request.auth.id", + "viewRule": "id = @request.auth.id", + "createRule": "", + "updateRule": "id = @request.auth.id", + "deleteRule": "id = @request.auth.id", + "options": { + "allowEmailAuth": true, + "allowOAuth2Auth": true, + "allowUsernameAuth": true, + "exceptEmailDomains": null, + "manageRule": null, + "minPasswordLength": 8, + "onlyEmailDomains": null, + "onlyVerified": false, + "requireEmail": false + } + }, + { + "id": "z3p974ainxjqlvs", + "created": "2024-07-29 10:02:48.334Z", + "updated": "2024-08-22 08:05:10.026Z", + "name": "domains", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "iuaerpl2", + "name": "domain", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "v98eebqq", + "name": "crontab", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "alc8e9ow", + "name": "access", + "type": "relation", + "required": false, + "presentable": false, + "unique": false, + "options": { + "collectionId": "4yzbv8urny5ja1e", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "topsc9bj", + "name": "certUrl", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "vixgq072", + "name": "certStableUrl", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "g3a3sza5", + "name": "privateKey", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "gr6iouny", + "name": "certificate", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "tk6vnrmn", + "name": "issuerCertificate", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "sjo6ibse", + "name": "csr", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "x03n1bkj", + "name": "expiredAt", + "type": "date", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": "", + "max": "" + } + }, + { + "system": false, + "id": "srybpixz", + "name": "targetType", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "aliyun-oss", + "aliyun-cdn", + "ssh" + ] + } + }, + { + "system": false, + "id": "xy7yk0mb", + "name": "targetAccess", + "type": "relation", + "required": false, + "presentable": false, + "unique": false, + "options": { + "collectionId": "4yzbv8urny5ja1e", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "6jqeyggw", + "name": "enabled", + "type": "bool", + "required": false, + "presentable": false, + "unique": false, + "options": {} + }, + { + "system": false, + "id": "hdsjcchf", + "name": "deployed", + "type": "bool", + "required": false, + "presentable": false, + "unique": false, + "options": {} + }, + { + "system": false, + "id": "aiya3rev", + "name": "rightnow", + "type": "bool", + "required": false, + "presentable": false, + "unique": false, + "options": {} + }, + { + "system": false, + "id": "ixznmhzc", + "name": "lastDeployedAt", + "type": "date", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": "", + "max": "" + } + }, + { + "system": false, + "id": "ghtlkn5j", + "name": "lastDeployment", + "type": "relation", + "required": false, + "presentable": false, + "unique": false, + "options": { + "collectionId": "0a1o4e6sstp694f", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + } + ], + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_4ABO6EQ` + "`" + ` ON ` + "`" + `domains` + "`" + ` (` + "`" + `domain` + "`" + `)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "4yzbv8urny5ja1e", + "created": "2024-07-29 10:04:39.685Z", + "updated": "2024-08-22 08:00:20.090Z", + "name": "access", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "geeur58v", + "name": "name", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "iql7jpwx", + "name": "config", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 2000000 + } + }, + { + "system": false, + "id": "hwy7m03o", + "name": "configType", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "aliyun", + "tencent", + "ssh" + ] + } + }, + { + "system": false, + "id": "lr33hiwg", + "name": "deleted", + "type": "date", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": "", + "max": "" + } + } + ], + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_wkoST0j` + "`" + ` ON ` + "`" + `access` + "`" + ` (` + "`" + `name` + "`" + `)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "0a1o4e6sstp694f", + "created": "2024-07-30 06:30:27.801Z", + "updated": "2024-08-21 04:13:40.056Z", + "name": "deployments", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "farvlzk7", + "name": "domain", + "type": "relation", + "required": false, + "presentable": false, + "unique": false, + "options": { + "collectionId": "z3p974ainxjqlvs", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "jx5f69i3", + "name": "log", + "type": "json", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSize": 2000000 + } + }, + { + "system": false, + "id": "qbxdtg9q", + "name": "phase", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "check", + "apply", + "deploy" + ] + } + }, + { + "system": false, + "id": "rglrp1hz", + "name": "phaseSuccess", + "type": "bool", + "required": false, + "presentable": false, + "unique": false, + "options": {} + }, + { + "system": false, + "id": "lt1g1blu", + "name": "deployedAt", + "type": "date", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": "", + "max": "" + } + } + ], + "indexes": [], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + } + ]` + + collections := []*models.Collection{} + if err := json.Unmarshal([]byte(jsonData), &collections); err != nil { + return err + } + + return daos.New(db).ImportCollections(collections, true, nil) + }, func(db dbx.Builder) error { + return nil + }) +} diff --git a/ui/public/imgs/providers/ssh.png b/ui/public/imgs/providers/ssh.png new file mode 100644 index 0000000000000000000000000000000000000000..d43cf03acb2b63496861ec0fecb37793aa9981cb GIT binary patch literal 1002 zcmeAS@N?(olHy`uVBq!ia0vp^DImM4wIg!tvun1O1nB^I^W&h<(Vz~c9GbnWv(in z9eOR>-g5OsZ4vYd$Q0a?v2>zzxl~8D0b5+*{il0p-aaGw?{f8id6{?5{>?1j_kND0 zaq-T{lO9|2fzi1VW9D%e{t!9?e^BRU9H+ z$!P~47$3T;q|I7!Ub{JH`SWsbj-9!RQ-q4LHwbw8>$E62NljA?>sY@`Qz=T*S|)2y*xVSvvz6h{ApiAKWquPyXvu2^`FV2N-sY@JMWNteBRl#_0v~H zm&PRb?_b{b(Q3;*yU3?ECf(BuNmtZ0dbeasT*Rb0o*g_jw`-ow@n1Tn%=_00hq{CB}e@ouNmwRcR=dXmHdN$AZ#9w+GR2nS#UqF0u$%@o# z^?Uzplktq70`k4sCczWw-3~?P@~5uaJfTkeY3AqK#~!)Gt~%#bHE-9d-ts3?K;pko zJf3oCUF*z$$LcHI24>G~-QtkHbF zE5DtY{7vWYy23=W*?YJBvp!M!!1@7K!JftEw3qmLocoy)r8r4%+6rI*ieB1Nw5?h5 zz}BEru}gay|7*Nlv&8)Q=bzqZ(|Gm(qcZS8uxG#5wMdWir|-_abm@W%e?#syot6qt zU~;GcCWmdniAU|ipBKcd-ga{F ); break; + case "ssh": + form = ( + { + setOpen(false); + }} + /> + ); + break; } const getOptionCls = (val: string) => { diff --git a/ui/src/components/certimate/AccessSSHForm.tsx b/ui/src/components/certimate/AccessSSHForm.tsx new file mode 100644 index 00000000..0d261b56 --- /dev/null +++ b/ui/src/components/certimate/AccessSSHForm.tsx @@ -0,0 +1,348 @@ +import { Access, accessFormType, SSHConfig } from "@/domain/access"; +import { useConfig } from "@/providers/config"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "../ui/form"; +import { Input } from "../ui/input"; +import { Button } from "../ui/button"; +import { Textarea } from "../ui/textarea"; +import { save } from "@/repository/access"; +import { ClientResponseError } from "pocketbase"; +import { PbErrorData } from "@/domain/base"; +import { readFileContent } from "@/lib/file"; + +const AccessSSHForm = ({ + data, + onAfterReq, +}: { + data?: Access; + onAfterReq: () => void; +}) => { + const { addAccess, updateAccess } = useConfig(); + const formSchema = z.object({ + id: z.string().optional(), + name: z.string().min(1).max(64), + configType: accessFormType, + host: z.string().ip({ + message: "请输入合法的IP地址", + }), + port: z.string().min(1).max(5), + username: z.string().min(1).max(64), + password: z.string().min(0).max(64), + key: z.string().min(0).max(20480), + keyFile: z.string().optional(), + command: z.string().min(1).max(2048), + certPath: z.string().min(0).max(2048), + keyPath: z.string().min(0).max(2048), + }); + + let config: SSHConfig = { + host: "127.0.0.1", + port: "22", + username: "root", + password: "", + key: "", + keyFile: "", + command: "sudo service nginx restart", + certPath: "/etc/nginx/ssl/certificate.crt", + keyPath: "/etc/nginx/ssl/private.key", + }; + if (data) config = data.config as SSHConfig; + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + id: data?.id, + name: data?.name, + configType: "ssh", + host: config.host, + port: config.port, + username: config.username, + password: config.password, + key: config.key, + keyFile: config.keyFile, + certPath: config.certPath, + keyPath: config.keyPath, + command: config.command, + }, + }); + + const onSubmit = async (data: z.infer) => { + console.log(data); + const req: Access = { + id: data.id as string, + name: data.name, + configType: data.configType, + config: { + host: data.host, + port: data.port, + username: data.username, + password: data.password, + key: data.key, + command: data.command, + certPath: data.certPath, + keyPath: data.keyPath, + }, + }; + + try { + const rs = await save(req); + + onAfterReq(); + + req.id = rs.id; + req.created = rs.created; + req.updated = rs.updated; + if (data.id) { + updateAccess(req); + return; + } + addAccess(req); + } catch (e) { + const err = e as ClientResponseError; + + Object.entries(err.response.data as PbErrorData).forEach( + ([key, value]) => { + form.setError(key as keyof z.infer, { + type: "manual", + message: value.message, + }); + } + ); + + return; + } + }; + + const handleFileChange = async ( + event: React.ChangeEvent + ) => { + const file = event.target.files?.[0]; + if (!file) return; + const content = await readFileContent(file); + form.setValue("key", content); + form.setValue("keyFile", ""); + }; + + return ( + <> +
+
+ { + e.stopPropagation(); + form.handleSubmit(onSubmit)(e); + }} + className="space-y-3" + > + ( + + 名称 + + + + + + + )} + /> + + ( + + 配置类型 + + + + + + + )} + /> + + ( + + 配置类型 + + + + + + + )} + /> +
+ ( + + 服务器IP + + + + + + + )} + /> + + ( + + SSH端口 + + + + + + + )} + /> +
+ + ( + + 用户名 + + + + + + + )} + /> + + ( + + 密码 + + + + + + + )} + /> + + ( + + )} + /> + + ( + + Key(使用证书登录) + + + + + + + )} + /> + + ( + + 证书上传路径 + + + + + + + )} + /> + + ( + + 私钥上传路径 + + + + + + + )} + /> + + ( + + Command + +