417 lines
12 KiB
Go
417 lines
12 KiB
Go
package command
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"github.com/Superdanda/hade/framework"
|
||
"github.com/Superdanda/hade/framework/cobra"
|
||
"github.com/Superdanda/hade/framework/contract"
|
||
"github.com/Superdanda/hade/framework/provider/ssh"
|
||
"github.com/Superdanda/hade/framework/util"
|
||
"github.com/pkg/sftp"
|
||
"log"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"runtime"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// initDeployCommand 为自动化部署的命令
|
||
func initDeployCommand() *cobra.Command {
|
||
deployCommand.AddCommand(deployFrontendCommand)
|
||
deployCommand.AddCommand(deployBackendCommand)
|
||
deployCommand.AddCommand(deployAllCommand)
|
||
deployCommand.AddCommand(deployRollbackCommand)
|
||
return deployCommand
|
||
}
|
||
|
||
var deployCommand = &cobra.Command{
|
||
Use: "deploy",
|
||
Short: "SSH自动化部署",
|
||
RunE: func(c *cobra.Command, args []string) error {
|
||
if len(args) == 0 {
|
||
c.Help()
|
||
}
|
||
return nil
|
||
},
|
||
}
|
||
|
||
// deployBackendCommand 部署后端
|
||
var deployBackendCommand = &cobra.Command{
|
||
Use: "backend",
|
||
Short: "部署后端",
|
||
RunE: func(c *cobra.Command, args []string) error {
|
||
container := c.GetContainer()
|
||
|
||
// 创建部署文件夹
|
||
deployFolder, err := createDeployFolder(container)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 编译后端到部署文件夹
|
||
if err := deployBuildBackend(c, deployFolder); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 上传部署文件夹并执行对应的shell
|
||
return deployUploadAction(deployFolder, container, "backend")
|
||
},
|
||
}
|
||
|
||
var deployFrontendCommand = &cobra.Command{
|
||
Use: "frontend",
|
||
Short: "部署前端",
|
||
RunE: func(c *cobra.Command, args []string) error {
|
||
container := c.GetContainer()
|
||
|
||
//创建部署的文件夹
|
||
deployFolder, err := createDeployFolder(container)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
//编译前端到部署文件夹
|
||
if err := deployBuildFrontend(c, deployFolder); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 上传部署文件夹并执行对应的shell
|
||
return deployUploadAction(deployFolder, container, "frontend")
|
||
},
|
||
}
|
||
|
||
// deployRollbackCommand 部署回滚
|
||
var deployRollbackCommand = &cobra.Command{
|
||
Use: "rollback",
|
||
Short: "部署回滚",
|
||
RunE: func(c *cobra.Command, args []string) error {
|
||
container := c.GetContainer()
|
||
|
||
if len(args) != 2 {
|
||
return errors.New("参数错误,请按照参数进行回滚 ./hade deploy rollback [version] [frontend/backend/all]")
|
||
}
|
||
|
||
version := args[0]
|
||
end := args[1]
|
||
|
||
// 获取版本信息
|
||
appService := container.MustMake(contract.AppKey).(contract.App)
|
||
deployFolder := filepath.Join(appService.DeployFolder(), version)
|
||
|
||
// 上传部署文件夹并执行对应的shell
|
||
return deployUploadAction(deployFolder, container, end)
|
||
},
|
||
}
|
||
|
||
var deployAllCommand = &cobra.Command{
|
||
Use: "all",
|
||
Short: "全部部署",
|
||
RunE: func(c *cobra.Command, args []string) error {
|
||
container := c.GetContainer()
|
||
|
||
deployFolder, err := createDeployFolder(container)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 编译前端
|
||
if err := deployBuildFrontend(c, deployFolder); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 编译后端
|
||
if err := deployBuildBackend(c, deployFolder); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 上传前端+后端,并执行对应的shell
|
||
return deployUploadAction(deployFolder, container, "all")
|
||
},
|
||
}
|
||
|
||
func deployBuildBackend(c *cobra.Command, deployFolder string) error {
|
||
container := c.GetContainer()
|
||
configService := container.MustMake(contract.ConfigKey).(contract.Config)
|
||
appService := container.MustMake(contract.AppKey).(contract.App)
|
||
envService := container.MustMake(contract.EnvKey).(contract.Env)
|
||
logger := container.MustMake(contract.LogKey).(contract.Log)
|
||
|
||
env := envService.AppEnv()
|
||
|
||
binFile := configService.GetAppName()
|
||
|
||
// 编译前端
|
||
path, err := exec.LookPath("go")
|
||
if err != nil {
|
||
log.Fatalln("hade go: 请在Path路径中先安装go")
|
||
}
|
||
|
||
// 组装命令
|
||
deployBinFile := filepath.Join(deployFolder, binFile)
|
||
cmd := exec.Command(path, "build", "-o", deployBinFile, "./")
|
||
cmd.Env = os.Environ()
|
||
// 设置GOOS和GOARCH
|
||
if configService.GetString("deploy.backend.goos") != "" {
|
||
cmd.Env = append(cmd.Env, "GOOS="+configService.GetString("deploy.backend.goos"))
|
||
}
|
||
if configService.GetString("deploy.backend.goarch") != "" {
|
||
cmd.Env = append(cmd.Env, "GOARCH="+configService.GetString("deploy.backend.goarch"))
|
||
}
|
||
|
||
// 执行命令
|
||
ctx := context.Background()
|
||
out, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
logger.Error(ctx, "go build err", map[string]interface{}{
|
||
"err": err,
|
||
"out": string(out),
|
||
})
|
||
return err
|
||
}
|
||
logger.Info(ctx, "编译成功", nil)
|
||
|
||
// 复制.env文件
|
||
if util.Exists(filepath.Join(appService.BaseFolder(), ".env")) {
|
||
if err := util.CopyFile(filepath.Join(appService.BaseFolder(), ".env"), filepath.Join(deployFolder, ".env")); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
// 复制config文件
|
||
deployConfigFolder := filepath.Join(deployFolder, "config", env)
|
||
if !util.Exists(deployConfigFolder) {
|
||
if err := os.MkdirAll(deployConfigFolder, os.ModePerm); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if err := util.CopyDir(filepath.Join(appService.ConfigFolder(), env), deployConfigFolder); err != nil {
|
||
return err
|
||
}
|
||
|
||
logger.Info(ctx, "build local ok", nil)
|
||
return nil
|
||
}
|
||
|
||
func deployUploadAction(deployFolder string, container framework.Container, end string) error {
|
||
// 1. 获取配置、SSH、日志服务实例
|
||
configService := container.MustMake(contract.ConfigKey).(contract.Config)
|
||
sshService := container.MustMake(contract.SSHKey).(contract.SSHService)
|
||
logger := container.MustMake(contract.LogKey).(contract.Log)
|
||
|
||
// 2. 获取所有部署节点配置,遍历所有deploy的服务器
|
||
deployNodes := configService.GetStringSlice("deploy.connections")
|
||
if len(deployNodes) == 0 {
|
||
return errors.New("deploy connections len is zero")
|
||
}
|
||
// 3. 读取远程部署文件夹路径
|
||
remoteFolder := configService.GetString("deploy.remote_folder")
|
||
if remoteFolder == "" {
|
||
return errors.New("remote folder is empty")
|
||
}
|
||
|
||
// 4. 初始化前置和后置操作
|
||
preActions := make([]string, 0, 1)
|
||
postActions := make([]string, 0, 1)
|
||
|
||
// 根据部署类型加载不同的前置和后置命令
|
||
if end == "frontend" || end == "all" {
|
||
preActions = append(preActions, configService.GetStringSlice("deploy.frontend.pre_action")...)
|
||
postActions = append(postActions, configService.GetStringSlice("deploy.frontend.post_action")...)
|
||
}
|
||
if end == "backend" || end == "all" {
|
||
preActions = append(preActions, configService.GetStringSlice("deploy.backend.pre_action")...)
|
||
postActions = append(postActions, configService.GetStringSlice("deploy.backend.post_action")...)
|
||
}
|
||
|
||
// 5. 遍历所有部署节点,进行上传操作
|
||
for _, node := range deployNodes {
|
||
sshClient, err := sshService.GetClient(ssh.WithConfigPath(node))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
client, err := sftp.NewClient(sshClient)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 6. 执行前置命令
|
||
for _, action := range preActions {
|
||
// 创建session
|
||
session, err := sshClient.NewSession()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
logger.Info(context.Background(), "execute pre action start", map[string]interface{}{
|
||
"cmd": action,
|
||
"connection": node,
|
||
})
|
||
// 执行命令,并且等待返回
|
||
bts, err := session.CombinedOutput(action)
|
||
if err != nil {
|
||
session.Close()
|
||
return err
|
||
}
|
||
session.Close()
|
||
// 执行前置命令成功
|
||
logger.Info(context.Background(), "execute pre action", map[string]interface{}{
|
||
"cmd": action,
|
||
"connection": node,
|
||
"out": strings.ReplaceAll(string(bts), "\n", ""),
|
||
})
|
||
}
|
||
|
||
// 7. 上传文件夹
|
||
if err := uploadFolderToSFTP(container, deployFolder, remoteFolder, client); err != nil {
|
||
logger.Info(context.Background(), "upload folder failed", map[string]interface{}{
|
||
"err": err,
|
||
})
|
||
return err
|
||
}
|
||
logger.Info(context.Background(), "upload folder success", nil)
|
||
|
||
// 8. 执行后置命令
|
||
for _, action := range postActions {
|
||
session, err := sshClient.NewSession()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
logger.Info(context.Background(), "execute post action start", map[string]interface{}{
|
||
"cmd": action,
|
||
"connection": node,
|
||
})
|
||
bts, err := session.CombinedOutput(action)
|
||
if err != nil {
|
||
session.Close()
|
||
return err
|
||
}
|
||
logger.Info(context.Background(), "execute post action finish", map[string]interface{}{
|
||
"cmd": action,
|
||
"connection": node,
|
||
"out": strings.ReplaceAll(string(bts), "\n", ""),
|
||
})
|
||
session.Close()
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// 上传部署文件夹
|
||
func uploadFolderToSFTP(container framework.Container, localFolder, remoteFolder string, client *sftp.Client) error {
|
||
logger := container.MustMake(contract.LogKey).(contract.Log)
|
||
// 遍历本地文件
|
||
return filepath.Walk(localFolder, func(path string, info os.FileInfo, err error) error {
|
||
// 获取除了folder前缀的后续文件名称
|
||
relPath := strings.Replace(path, localFolder, "", 1)
|
||
if relPath == "" {
|
||
return nil
|
||
}
|
||
// 如果是遍历到了一个目录
|
||
var remotePath string
|
||
if runtime.GOOS == "windows" {
|
||
relPath := strings.Replace(path, localFolder, "", 1)
|
||
remotePath = filepath.ToSlash(filepath.Join(remoteFolder, relPath))
|
||
} else {
|
||
remotePath = filepath.Join(remoteFolder, relPath)
|
||
}
|
||
if info.IsDir() {
|
||
|
||
logger.Info(context.Background(), "mkdir: "+remotePath, nil)
|
||
// 创建这个目录
|
||
return client.MkdirAll(remotePath)
|
||
}
|
||
|
||
// 打开本地的文件
|
||
rf, err := os.Open(filepath.Join(localFolder, relPath))
|
||
if err != nil {
|
||
return errors.New("read file " + filepath.Join(localFolder, relPath) + " error:" + err.Error())
|
||
}
|
||
defer rf.Close()
|
||
// 检查文件大小
|
||
rfStat, err := rf.Stat()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
// 打开/创建远端文件
|
||
f, err := client.Create(remotePath)
|
||
if err != nil {
|
||
return errors.New("create file " + remotePath + " error:" + err.Error())
|
||
}
|
||
defer f.Close()
|
||
|
||
// 大于2M的文件显示进度
|
||
if rfStat.Size() > 2*1024*1024 {
|
||
logger.Info(context.Background(), "upload local file: "+filepath.Join(localFolder, relPath)+
|
||
" to remote file: "+remotePath+" start", nil)
|
||
// 开启一个goroutine来不断计算进度
|
||
go func(localFile, remoteFile string) {
|
||
// 每10s计算一次
|
||
ticker := time.NewTicker(2 * time.Second)
|
||
for range ticker.C {
|
||
// 获取远端文件信息
|
||
remoteFileInfo, err := client.Stat(remoteFile)
|
||
if err != nil {
|
||
logger.Error(context.Background(), "stat error", map[string]interface{}{
|
||
"err": err,
|
||
"remote_file": remoteFile,
|
||
})
|
||
continue
|
||
}
|
||
// 如果远端文件大小等于本地文件大小,说明已经结束了
|
||
size := remoteFileInfo.Size()
|
||
if size >= rfStat.Size() {
|
||
break
|
||
}
|
||
// 计算进度并且打印进度
|
||
percent := int(size * 100 / rfStat.Size())
|
||
logger.Info(context.Background(), "upload local file: "+filepath.Join(localFolder, relPath)+
|
||
" to remote file: "+remotePath+fmt.Sprintf(" %v%% %v/%v", percent, size, rfStat.Size()), nil)
|
||
}
|
||
}(filepath.Join(localFolder, relPath), remotePath)
|
||
}
|
||
|
||
// 将本地文件并发读取到远端文件
|
||
if _, err := f.ReadFromWithConcurrency(rf, 10); err != nil {
|
||
return errors.New("Write file " + remotePath + " error:" + err.Error())
|
||
}
|
||
// 记录成功信息
|
||
logger.Info(context.Background(), "upload local file: "+filepath.Join(localFolder, relPath)+
|
||
" to remote file: "+remotePath+" finish", nil)
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func deployBuildFrontend(c *cobra.Command, deployFolder string) error {
|
||
container := c.GetContainer()
|
||
appService := container.MustMake(contract.AppKey).(contract.App)
|
||
|
||
if err := buildFrontendCommand.RunE(c, []string{}); err != nil {
|
||
return err
|
||
}
|
||
|
||
frontendFolder := filepath.Join(deployFolder, "dist")
|
||
if err := os.Mkdir(frontendFolder, os.ModePerm); err != nil {
|
||
return err
|
||
}
|
||
|
||
buildFolder := filepath.Join(appService.BaseFolder(), "dist")
|
||
if err := util.CopyDir(buildFolder, frontendFolder); err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func createDeployFolder(container framework.Container) (string, error) {
|
||
appService := container.MustMake(contract.AppKey).(contract.App)
|
||
deployFolder := appService.DeployFolder()
|
||
|
||
deployVersion := time.Now().Format("20060102150405")
|
||
versionFolder := filepath.Join(deployFolder, deployVersion)
|
||
if !util.Exists(versionFolder) {
|
||
return versionFolder, os.Mkdir(versionFolder, os.ModePerm)
|
||
}
|
||
return versionFolder, nil
|
||
}
|