framework1/framework/command/deploy.go

417 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}