feat: 创建网站、应用任务化

This commit is contained in:
zhengkunwang223 2024-07-29 18:09:57 +08:00
parent 865b6cba3f
commit 0e5d6e825e
42 changed files with 925 additions and 471 deletions

1
.gitignore vendored
View File

@ -48,6 +48,7 @@ agent/utils/xpack/xpack_xpack.go
core/xpack core/xpack
core/router/entry_xpack.go core/router/entry_xpack.go
core/server/init_xpack.go core/server/init_xpack.go
xpack
.history/ .history/
dist/ dist/

View File

@ -161,14 +161,11 @@ func (b *BaseApi) InstallApp(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
tx, ctx := helper.GetTxAndContext() install, err := appService.Install(req)
install, err := appService.Install(ctx, req)
if err != nil { if err != nil {
tx.Rollback()
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return
} }
tx.Commit()
helper.SuccessWithData(c, install) helper.SuccessWithData(c, install)
} }

View File

@ -1,8 +1,6 @@
package v1 package v1
import ( import (
"encoding/base64"
"github.com/1Panel-dev/1Panel/agent/app/api/v1/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/dto/request"
@ -78,14 +76,6 @@ func (b *BaseApi) CreateWebsite(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if len(req.FtpPassword) != 0 {
pass, err := base64.StdEncoding.DecodeString(req.FtpPassword)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.FtpPassword = string(pass)
}
err := websiteService.CreateWebsite(req) err := websiteService.CreateWebsite(req)
if err != nil { if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)

View File

@ -128,6 +128,7 @@ type FileReadByLineReq struct {
ID uint `json:"ID"` ID uint `json:"ID"`
Name string `json:"name"` Name string `json:"name"`
Latest bool `json:"latest"` Latest bool `json:"latest"`
TaskID string `json:"taskID"`
} }
type FileExistReq struct { type FileExistReq struct {

View File

@ -0,0 +1 @@
package request

View File

@ -31,6 +31,7 @@ type WebsiteCreate struct {
FtpPassword string `json:"ftpPassword"` FtpPassword string `json:"ftpPassword"`
RuntimeID uint `json:"runtimeID"` RuntimeID uint `json:"runtimeID"`
TaskID string `json:"taskID"`
RuntimeConfig RuntimeConfig
} }

View File

@ -0,0 +1 @@
package response

17
agent/app/model/task.go Normal file
View File

@ -0,0 +1,17 @@
package model
import "time"
type Task struct {
ID string `gorm:"primarykey;" json:"id"`
Name string `json:"name"`
Type string `json:"type"`
LogFile string `json:"logFile"`
Status string `json:"status"`
ErrorMsg string `json:"errorMsg"`
OperationLogID uint `json:"operationLogID"`
ResourceID uint `json:"resourceID"`
CurrentStep string `json:"currentStep"`
EndAt time.Time `json:"endAt"`
CreatedAt time.Time `json:"createdAt"`
}

55
agent/app/repo/task.go Normal file
View File

@ -0,0 +1,55 @@
package repo
import (
"context"
"github.com/1Panel-dev/1Panel/agent/app/model"
"gorm.io/gorm"
)
type TaskRepo struct {
}
type ITaskRepo interface {
Create(ctx context.Context, task *model.Task) error
GetFirst(opts ...DBOption) (model.Task, error)
Page(page, size int, opts ...DBOption) (int64, []model.Task, error)
Update(ctx context.Context, task *model.Task) error
WithByID(id string) DBOption
}
func NewITaskRepo() ITaskRepo {
return &TaskRepo{}
}
func (t TaskRepo) WithByID(id string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("id = ?", id)
}
}
func (t TaskRepo) Create(ctx context.Context, task *model.Task) error {
return getTx(ctx).Create(&task).Error
}
func (t TaskRepo) GetFirst(opts ...DBOption) (model.Task, error) {
var task model.Task
db := getDb(opts...).Model(&model.Task{})
if err := db.First(&task).Error; err != nil {
return task, err
}
return task, nil
}
func (t TaskRepo) Page(page, size int, opts ...DBOption) (int64, []model.Task, error) {
var tasks []model.Task
db := getDb(opts...).Model(&model.Task{})
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&tasks).Error
return count, tasks, err
}
func (t TaskRepo) Update(ctx context.Context, task *model.Task) error {
return getTx(ctx).Save(&task).Error
}

View File

@ -5,18 +5,12 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response" "github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
@ -27,7 +21,14 @@ import (
http2 "github.com/1Panel-dev/1Panel/agent/utils/http" http2 "github.com/1Panel-dev/1Panel/agent/utils/http"
httpUtil "github.com/1Panel-dev/1Panel/agent/utils/http" httpUtil "github.com/1Panel-dev/1Panel/agent/utils/http"
"github.com/1Panel-dev/1Panel/agent/utils/xpack" "github.com/1Panel-dev/1Panel/agent/utils/xpack"
"github.com/google/uuid"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"net/http"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
) )
type AppService struct { type AppService struct {
@ -38,7 +39,7 @@ type IAppService interface {
GetAppTags() ([]response.TagDTO, error) GetAppTags() ([]response.TagDTO, error)
GetApp(key string) (*response.AppDTO, error) GetApp(key string) (*response.AppDTO, error)
GetAppDetail(appId uint, version, appType string) (response.AppDetailDTO, error) GetAppDetail(appId uint, version, appType string) (response.AppDetailDTO, error)
Install(ctx context.Context, req request.AppInstallCreate) (*model.AppInstall, error) Install(req request.AppInstallCreate) (*model.AppInstall, error)
SyncAppListFromRemote() error SyncAppListFromRemote() error
GetAppUpdate() (*response.AppUpdateRes, error) GetAppUpdate() (*response.AppUpdateRes, error)
GetAppDetailByID(id uint) (*response.AppDetailDTO, error) GetAppDetailByID(id uint) (*response.AppDetailDTO, error)
@ -295,7 +296,7 @@ func (a AppService) GetIgnoredApp() ([]response.IgnoredApp, error) {
return res, nil return res, nil
} }
func (a AppService) Install(ctx context.Context, req request.AppInstallCreate) (appInstall *model.AppInstall, err error) { func (a AppService) Install(req request.AppInstallCreate) (appInstall *model.AppInstall, err error) {
if err = docker.CreateDefaultDockerNetwork(); err != nil { if err = docker.CreateDefaultDockerNetwork(); err != nil {
err = buserr.WithDetail(constant.Err1PanelNetworkFailed, err.Error(), nil) err = buserr.WithDetail(constant.Err1PanelNetworkFailed, err.Error(), nil)
return return
@ -423,14 +424,6 @@ func (a AppService) Install(ctx context.Context, req request.AppInstallCreate) (
} }
appInstall.DockerCompose = string(composeByte) appInstall.DockerCompose = string(composeByte)
defer func() {
if err != nil {
hErr := handleAppInstallErr(ctx, appInstall)
if hErr != nil {
global.LOG.Errorf("delete app dir error %s", hErr.Error())
}
}
}()
if hostName, ok := req.Params["PANEL_DB_HOST"]; ok { if hostName, ok := req.Params["PANEL_DB_HOST"]; ok {
database, _ := databaseRepo.Get(commonRepo.WithByName(hostName.(string))) database, _ := databaseRepo.Get(commonRepo.WithByName(hostName.(string)))
if !reflect.DeepEqual(database, model.Database{}) { if !reflect.DeepEqual(database, model.Database{}) {
@ -445,29 +438,48 @@ func (a AppService) Install(ctx context.Context, req request.AppInstallCreate) (
} }
appInstall.Env = string(paramByte) appInstall.Env = string(paramByte)
if err = appInstallRepo.Create(ctx, appInstall); err != nil { if err = appInstallRepo.Create(context.Background(), appInstall); err != nil {
return return
} }
if err = createLink(ctx, app, appInstall, req.Params); err != nil {
return taskID := uuid.New().String()
} installTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskCreate, task.TaskScopeApp, taskID)
go func() {
defer func() {
if err != nil { if err != nil {
return
}
if err = createLink(context.Background(), installTask, app, appInstall, req.Params); err != nil {
return
}
installApp := func(t *task.Task) error {
if err = copyData(t, app, appDetail, appInstall, req); err != nil {
return err
}
if err = runScript(t, appInstall, "init"); err != nil {
return err
}
upApp(t, appInstall, req.PullImage)
updateToolApp(appInstall)
return nil
}
handleAppStatus := func() {
appInstall.Status = constant.UpErr appInstall.Status = constant.UpErr
appInstall.Message = err.Error() appInstall.Message = installTask.Task.ErrorMsg
_ = appInstallRepo.Save(context.Background(), appInstall)
}
installTask.AddSubTask(task.GetTaskName(appInstall.Name, task.TaskInstall, task.TaskScopeApp), installApp, handleAppStatus)
go func() {
if taskErr := installTask.Execute(); taskErr != nil {
appInstall.Status = constant.InstallErr
appInstall.Message = taskErr.Error()
_ = appInstallRepo.Save(context.Background(), appInstall) _ = appInstallRepo.Save(context.Background(), appInstall)
} }
}() }()
if err = copyData(app, appDetail, appInstall, req); err != nil {
return
}
if err = runScript(appInstall, "init"); err != nil {
return
}
upApp(appInstall, req.PullImage)
}()
go updateToolApp(appInstall)
return return
} }

View File

@ -5,6 +5,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/agent/app/task"
"math" "math"
"net/http" "net/http"
"os" "os"
@ -130,9 +131,13 @@ var ToolKeys = map[string]uint{
"minio": 9001, "minio": 9001,
} }
func createLink(ctx context.Context, app model.App, appInstall *model.AppInstall, params map[string]interface{}) error { func createLink(ctx context.Context, installTask *task.Task, app model.App, appInstall *model.AppInstall, params map[string]interface{}) error {
deleteAppLink := func() {
_ = deleteLink(ctx, appInstall, true, true, true)
}
var dbConfig dto.AppDatabase var dbConfig dto.AppDatabase
if DatabaseKeys[app.Key] > 0 { if DatabaseKeys[app.Key] > 0 {
handleDataBaseApp := func(task *task.Task) error {
database := &model.Database{ database := &model.Database{
AppInstallID: appInstall.ID, AppInstallID: appInstall.ID,
Name: appInstall.Name, Name: appInstall.Name,
@ -199,9 +204,9 @@ func createLink(ctx context.Context, app model.App, appInstall *model.AppInstall
database.Password = password.(string) database.Password = password.(string)
} }
} }
if err := databaseRepo.Create(ctx, database); err != nil { return databaseRepo.Create(ctx, database)
return err
} }
installTask.AddSubTask(i18n.GetMsgByKey("HandleDatabaseApp"), handleDataBaseApp, deleteAppLink)
} }
if ToolKeys[app.Key] > 0 { if ToolKeys[app.Key] > 0 {
if app.Key == "minio" { if app.Key == "minio" {
@ -231,6 +236,7 @@ func createLink(ctx context.Context, app model.App, appInstall *model.AppInstall
} }
if !reflect.DeepEqual(dbConfig, dto.AppDatabase{}) && dbConfig.ServiceName != "" { if !reflect.DeepEqual(dbConfig, dto.AppDatabase{}) && dbConfig.ServiceName != "" {
createAppDataBase := func(rootTask *task.Task) error {
hostName := params["PANEL_DB_HOST_NAME"] hostName := params["PANEL_DB_HOST_NAME"]
if hostName == nil || hostName.(string) == "" { if hostName == nil || hostName.(string) == "" {
return nil return nil
@ -289,7 +295,6 @@ func createLink(ctx context.Context, app model.App, appInstall *model.AppInstall
resourceId = mysqldb.ID resourceId = mysqldb.ID
} }
} }
} }
var installResource model.AppInstallResource var installResource model.AppInstallResource
installResource.ResourceId = resourceId installResource.ResourceId = resourceId
@ -301,25 +306,9 @@ func createLink(ctx context.Context, app model.App, appInstall *model.AppInstall
} }
installResource.Key = database.Type installResource.Key = database.Type
installResource.From = database.From installResource.From = database.From
if err := appInstallResourceRepo.Create(ctx, &installResource); err != nil { return appInstallResourceRepo.Create(ctx, &installResource)
return err
} }
} installTask.AddSubTask(task.GetTaskName(dbConfig.DbName, task.TaskCreate, task.TaskScopeDatabase), createAppDataBase, deleteAppLink)
return nil
}
func handleAppInstallErr(ctx context.Context, install *model.AppInstall) error {
op := files.NewFileOp()
appDir := install.GetPath()
dir, _ := os.Stat(appDir)
if dir != nil {
_, _ = compose.Down(install.GetComposePath())
if err := op.DeleteDir(appDir); err != nil {
return err
}
}
if err := deleteLink(ctx, install, true, true, true); err != nil {
return err
} }
return nil return nil
} }
@ -333,7 +322,8 @@ func deleteAppInstall(install model.AppInstall, deleteBackup bool, forceDelete b
if err != nil && !forceDelete { if err != nil && !forceDelete {
return handleErr(install, err, out) return handleErr(install, err, out)
} }
if err = runScript(&install, "uninstall"); err != nil { //TODO use task
if err = runScript(nil, &install, "uninstall"); err != nil {
_, _ = compose.Up(install.GetComposePath()) _, _ = compose.Up(install.GetComposePath())
return err return err
} }
@ -652,7 +642,8 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
return return
} }
if upErr = runScript(&install, "upgrade"); upErr != nil { //TODO use task
if upErr = runScript(nil, &install, "upgrade"); upErr != nil {
return return
} }
@ -800,7 +791,7 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
return return
} }
func copyData(app model.App, appDetail model.AppDetail, appInstall *model.AppInstall, req request.AppInstallCreate) (err error) { func copyData(task *task.Task, app model.App, appDetail model.AppDetail, appInstall *model.AppInstall, req request.AppInstallCreate) (err error) {
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
appResourceDir := path.Join(constant.AppResourceDir, app.Resource) appResourceDir := path.Join(constant.AppResourceDir, app.Resource)
@ -853,7 +844,7 @@ func copyData(app model.App, appDetail model.AppDetail, appInstall *model.AppIns
return return
} }
func runScript(appInstall *model.AppInstall, operate string) error { func runScript(task *task.Task, appInstall *model.AppInstall, operate string) error {
workDir := appInstall.GetPath() workDir := appInstall.GetPath()
scriptPath := "" scriptPath := ""
switch operate { switch operate {
@ -867,15 +858,17 @@ func runScript(appInstall *model.AppInstall, operate string) error {
if !files.NewFileOp().Stat(scriptPath) { if !files.NewFileOp().Stat(scriptPath) {
return nil return nil
} }
logStr := i18n.GetWithName("ExecShell", operate)
task.LogStart(logStr)
out, err := cmd.ExecScript(scriptPath, workDir) out, err := cmd.ExecScript(scriptPath, workDir)
if err != nil { if err != nil {
if out != "" { if out != "" {
errMsg := fmt.Sprintf("run script %s error %s", scriptPath, out) err = errors.New(out)
global.LOG.Error(errMsg)
return errors.New(errMsg)
} }
task.LogFailedWithErr(logStr, err)
return err return err
} }
task.LogSuccess(logStr)
return nil return nil
} }
@ -905,15 +898,25 @@ func checkContainerNameIsExist(containerName, appDir string) (bool, error) {
return false, nil return false, nil
} }
func upApp(appInstall *model.AppInstall, pullImages bool) { func upApp(task *task.Task, appInstall *model.AppInstall, pullImages bool) {
upProject := func(appInstall *model.AppInstall) (err error) { upProject := func(appInstall *model.AppInstall) (err error) {
var ( var (
out string out string
errMsg string errMsg string
) )
if pullImages && appInstall.App.Type != "php" { if pullImages && appInstall.App.Type != "php" {
out, err = compose.Pull(appInstall.GetComposePath()) projectName := strings.ToLower(appInstall.Name)
envByte, err := files.NewFileOp().GetContent(appInstall.GetEnvPath())
if err != nil { if err != nil {
return err
}
images, err := composeV2.GetDockerComposeImages(projectName, envByte, []byte(appInstall.DockerCompose))
if err != nil {
return err
}
for _, image := range images {
task.Log(i18n.GetWithName("PullImageStart", image))
if out, err = cmd.ExecWithTimeOut("docker pull "+image, 20*time.Minute); err != nil {
if out != "" { if out != "" {
if strings.Contains(out, "no such host") { if strings.Contains(out, "no such host") {
errMsg = i18n.GetMsgByKey("ErrNoSuchHost") + ":" errMsg = i18n.GetMsgByKey("ErrNoSuchHost") + ":"
@ -921,22 +924,33 @@ func upApp(appInstall *model.AppInstall, pullImages bool) {
if strings.Contains(out, "timeout") { if strings.Contains(out, "timeout") {
errMsg = i18n.GetMsgByKey("ErrImagePullTimeOut") + ":" errMsg = i18n.GetMsgByKey("ErrImagePullTimeOut") + ":"
} }
}
appInstall.Message = errMsg + out appInstall.Message = errMsg + out
} task.LogFailedWithErr(i18n.GetMsgByKey("PullImage"), err)
return err return err
} else {
task.Log(i18n.GetMsgByKey("PullImageSuccess"))
} }
} }
}
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("Run"), i18n.GetMsgByKey("App"))
task.Log(logStr)
out, err = compose.Up(appInstall.GetComposePath()) out, err = compose.Up(appInstall.GetComposePath())
if err != nil { if err != nil {
if out != "" { if out != "" {
appInstall.Message = errMsg + out appInstall.Message = errMsg + out
err = errors.New(out)
} }
task.LogFailedWithErr(logStr, err)
return err return err
} }
task.LogSuccess(logStr)
return return
} }
if err := upProject(appInstall); err != nil { if err := upProject(appInstall); err != nil {
if appInstall.Message == "" {
appInstall.Message = err.Error()
}
appInstall.Status = constant.UpErr appInstall.Status = constant.UpErr
} else { } else {
appInstall.Status = constant.Running appInstall.Status = constant.Running
@ -944,14 +958,13 @@ func upApp(appInstall *model.AppInstall, pullImages bool) {
exist, _ := appInstallRepo.GetFirst(commonRepo.WithByID(appInstall.ID)) exist, _ := appInstallRepo.GetFirst(commonRepo.WithByID(appInstall.ID))
if exist.ID > 0 { if exist.ID > 0 {
containerNames, err := getContainerNames(*appInstall) containerNames, err := getContainerNames(*appInstall)
if err != nil { if err == nil {
return
}
if len(containerNames) > 0 { if len(containerNames) > 0 {
appInstall.ContainerName = strings.Join(containerNames, ",") appInstall.ContainerName = strings.Join(containerNames, ",")
} }
_ = appInstallRepo.Save(context.Background(), appInstall) _ = appInstallRepo.Save(context.Background(), appInstall)
} }
}
} }
func rebuildApp(appInstall model.AppInstall) error { func rebuildApp(appInstall model.AppInstall) error {

View File

@ -43,4 +43,6 @@ var (
phpExtensionsRepo = repo.NewIPHPExtensionsRepo() phpExtensionsRepo = repo.NewIPHPExtensionsRepo()
favoriteRepo = repo.NewIFavoriteRepo() favoriteRepo = repo.NewIFavoriteRepo()
taskRepo = repo.NewITaskRepo()
) )

View File

@ -466,7 +466,13 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
return nil, fmt.Errorf("handle ungzip file %s failed, err: %v", fileGzPath, err) return nil, fmt.Errorf("handle ungzip file %s failed, err: %v", fileGzPath, err)
} }
} }
case "image-pull", "image-push", "image-build", "compose-create": case constant.TypeTask:
task, err := taskRepo.GetFirst(taskRepo.WithByID(req.TaskID))
if err != nil {
return nil, err
}
logFilePath = task.LogFile
case constant.TypeImagePull, constant.TypeImagePush, constant.TypeImageBuild, constant.TypeComposeCreate:
logFilePath = path.Join(global.CONF.System.TmpDir, fmt.Sprintf("docker_logs/%s", req.Name)) logFilePath = path.Join(global.CONF.System.TmpDir, fmt.Sprintf("docker_logs/%s", req.Name))
} }

View File

@ -5,10 +5,12 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/x509" "crypto/x509"
"encoding/base64"
"encoding/json" "encoding/json"
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/agent/app/task"
"os" "os"
"path" "path"
"reflect" "reflect"
@ -206,6 +208,13 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
if exist, _ := websiteRepo.GetBy(websiteRepo.WithAlias(alias)); len(exist) > 0 { if exist, _ := websiteRepo.GetBy(websiteRepo.WithAlias(alias)); len(exist) > 0 {
return buserr.New(constant.ErrAliasIsExist) return buserr.New(constant.ErrAliasIsExist)
} }
if len(create.FtpPassword) != 0 {
pass, err := base64.StdEncoding.DecodeString(create.FtpPassword)
if err != nil {
return err
}
create.FtpPassword = string(pass)
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil { if err != nil {
@ -249,23 +258,15 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
runtime *model.Runtime runtime *model.Runtime
) )
defer func() { createTask, err := task.NewTaskWithOps(create.PrimaryDomain, task.TaskCreate, task.TaskScopeWebsite, create.TaskID)
if err != nil { if err != nil {
if website.AppInstallID > 0 { return err
req := request.AppInstalledOperate{
InstallId: website.AppInstallID,
Operate: constant.Delete,
ForceDelete: true,
} }
if err := NewIAppInstalledService().Operate(req); err != nil {
global.LOG.Errorf(err.Error())
}
}
}
}()
var proxy string var proxy string
switch create.Type { switch create.Type {
case constant.Deployment: case constant.Deployment:
if create.AppType == constant.NewApp { if create.AppType == constant.NewApp {
var ( var (
@ -276,13 +277,10 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
req.AppDetailId = create.AppInstall.AppDetailId req.AppDetailId = create.AppInstall.AppDetailId
req.Params = create.AppInstall.Params req.Params = create.AppInstall.Params
req.AppContainerConfig = create.AppInstall.AppContainerConfig req.AppContainerConfig = create.AppInstall.AppContainerConfig
tx, installCtx := getTxAndContext() install, err = NewIAppService().Install(req)
install, err = NewIAppService().Install(installCtx, req)
if err != nil { if err != nil {
tx.Rollback()
return err return err
} }
tx.Commit()
appInstall = install appInstall = install
website.AppInstallID = install.ID website.AppInstallID = install.ID
website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort) website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort)
@ -292,9 +290,13 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
if err != nil { if err != nil {
return err return err
} }
configApp := func(t *task.Task) error {
appInstall = &install appInstall = &install
website.AppInstallID = appInstall.ID website.AppInstallID = appInstall.ID
website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort) website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort)
return nil
}
createTask.AddSubTask(i18n.GetMsgByKey("ConfigApp"), configApp, nil)
} }
case constant.Runtime: case constant.Runtime:
runtime, err = runtimeRepo.GetFirst(commonRepo.WithByID(create.RuntimeID)) runtime, err = runtimeRepo.GetFirst(commonRepo.WithByID(create.RuntimeID))
@ -302,9 +304,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
return err return err
} }
website.RuntimeID = runtime.ID website.RuntimeID = runtime.ID
switch runtime.Type { if runtime.Type == constant.RuntimePHP {
case constant.RuntimePHP:
if runtime.Resource == constant.ResourceAppstore {
var ( var (
req request.AppInstallCreate req request.AppInstallCreate
install *model.AppInstall install *model.AppInstall
@ -316,13 +316,10 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
req.Params["IMAGE_NAME"] = runtime.Image req.Params["IMAGE_NAME"] = runtime.Image
req.AppContainerConfig = create.AppInstall.AppContainerConfig req.AppContainerConfig = create.AppInstall.AppContainerConfig
req.Params["PANEL_WEBSITE_DIR"] = path.Join(nginxInstall.GetPath(), "/www") req.Params["PANEL_WEBSITE_DIR"] = path.Join(nginxInstall.GetPath(), "/www")
tx, installCtx := getTxAndContext() install, err = NewIAppService().Install(req)
install, err = NewIAppService().Install(installCtx, req)
if err != nil { if err != nil {
tx.Rollback()
return err return err
} }
tx.Commit()
website.AppInstallID = install.ID website.AppInstallID = install.ID
appInstall = install appInstall = install
website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort) website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort)
@ -339,25 +336,35 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo: case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo:
website.Proxy = fmt.Sprintf("127.0.0.1:%d", runtime.Port) website.Proxy = fmt.Sprintf("127.0.0.1:%d", runtime.Port)
} }
}
if err = configDefaultNginx(website, domains, appInstall, runtime); err != nil {
return err
}
if len(create.FtpUser) != 0 && len(create.FtpPassword) != 0 { if len(create.FtpUser) != 0 && len(create.FtpPassword) != 0 {
createFtpUser := func(t *task.Task) error {
indexDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "index") indexDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "index")
itemID, err := NewIFtpService().Create(dto.FtpCreate{User: create.FtpUser, Password: create.FtpPassword, Path: indexDir}) itemID, err := NewIFtpService().Create(dto.FtpCreate{User: create.FtpUser, Password: create.FtpPassword, Path: indexDir})
if err != nil { if err != nil {
global.LOG.Errorf("create ftp for website failed, err: %v", err) createTask.Log(fmt.Sprintf("create ftp for website failed, err: %v", err))
} }
website.FtpID = itemID website.FtpID = itemID
return nil
}
deleteFtpUser := func() {
if website.FtpID > 0 {
req := dto.BatchDeleteReq{Ids: []uint{website.FtpID}}
if err = NewIFtpService().Delete(req); err != nil {
createTask.Log(err.Error())
}
}
}
createTask.AddSubTask(i18n.GetWithName("ConfigFTP", create.FtpUser), createFtpUser, deleteFtpUser)
} }
configNginx := func(t *task.Task) error {
if err = configDefaultNginx(website, domains, appInstall, runtime); err != nil {
return err
}
if err = createWafConfig(website, domains); err != nil { if err = createWafConfig(website, domains); err != nil {
return err return err
} }
tx, ctx := helper.GetTxAndContext() tx, ctx := helper.GetTxAndContext()
defer tx.Rollback() defer tx.Rollback()
if err = websiteRepo.Create(ctx, website); err != nil { if err = websiteRepo.Create(ctx, website); err != nil {
@ -371,6 +378,15 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
} }
tx.Commit() tx.Commit()
return nil return nil
}
deleteWebsite := func() {
_ = deleteWebsiteFolder(nginxInstall, website)
}
createTask.AddSubTask(i18n.GetMsgByKey("ConfigOpenresty"), configNginx, deleteWebsite)
return createTask.Execute()
} }
func (w WebsiteService) OpWebsite(req request.WebsiteOp) error { func (w WebsiteService) OpWebsite(req request.WebsiteOp) error {

View File

@ -220,10 +220,9 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
if err != nil { if err != nil {
return err return err
} }
if err := createWebsiteFolder(nginxInstall, website, runtime); err != nil { if err = createWebsiteFolder(nginxInstall, website, runtime); err != nil {
return err return err
} }
nginxFileName := website.Alias + ".conf" nginxFileName := website.Alias + ".conf"
configPath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "conf.d", nginxFileName) configPath := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "conf.d", nginxFileName)
nginxContent := string(nginx_conf.WebsiteDefault) nginxContent := string(nginx_conf.WebsiteDefault)
@ -284,15 +283,13 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
} }
config.FilePath = configPath config.FilePath = configPath
if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return err return err
} }
if err := opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil { if err = opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil {
_ = deleteWebsiteFolder(nginxInstall, website)
return err return err
} }
if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil { if err = opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
_ = deleteWebsiteFolder(nginxInstall, website)
return err return err
} }
return nil return nil

View File

@ -3,28 +3,35 @@ package task
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/google/uuid"
"log" "log"
"os" "os"
"path" "path"
"strconv" "strconv"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/i18n"
) )
type ActionFunc func() error type ActionFunc func(*Task) error
type RollbackFunc func() type RollbackFunc func()
type Task struct { type Task struct {
Name string Name string
TaskID string
Logger *log.Logger Logger *log.Logger
SubTasks []*SubTask SubTasks []*SubTask
Rollbacks []RollbackFunc Rollbacks []RollbackFunc
logFile *os.File logFile *os.File
taskRepo repo.ITaskRepo
Task *model.Task
ParentID string
} }
type SubTask struct { type SubTask struct {
RootTask *Task
Name string Name string
Retry int Retry int
Timeout time.Duration Timeout time.Duration
@ -33,51 +40,107 @@ type SubTask struct {
Error error Error error
} }
func NewTask(name string, taskType string) (*Task, error) { const (
logPath := path.Join(constant.LogDir, taskType) TaskInstall = "TaskInstall"
//TODO 增加插入到日志表的逻辑 TaskUninstall = "TaskUninstall"
TaskCreate = "TaskCreate"
TaskDelete = "TaskDelete"
TaskUpgrade = "TaskUpgrade"
TaskUpdate = "TaskUpdate"
TaskRestart = "TaskRestart"
)
const (
TaskScopeWebsite = "Website"
TaskScopeApp = "App"
TaskScopeRuntime = "Runtime"
TaskScopeDatabase = "Database"
)
const (
TaskSuccess = "Success"
TaskFailed = "Failed"
)
func GetTaskName(resourceName, operate, scope string) string {
return fmt.Sprintf("%s%s [%s]", i18n.GetMsgByKey(operate), i18n.GetMsgByKey(scope), resourceName)
}
func NewTaskWithOps(resourceName, operate, scope, taskID string) (*Task, error) {
return NewTask(GetTaskName(resourceName, operate, scope), scope, taskID)
}
func NewChildTask(name, taskType, parentTaskID string) (*Task, error) {
task, err := NewTask(name, taskType, "")
if err != nil {
return nil, err
}
task.ParentID = parentTaskID
return task, nil
}
func NewTask(name, taskType, taskID string) (*Task, error) {
if taskID == "" {
taskID = uuid.New().String()
}
logDir := path.Join(constant.LogDir, taskType)
if _, err := os.Stat(logDir); os.IsNotExist(err) {
if err = os.MkdirAll(logDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create log directory: %w", err)
}
}
logPath := path.Join(constant.LogDir, taskType, taskID+".log")
file, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0666) file, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to open log file: %w", err) return nil, fmt.Errorf("failed to open log file: %w", err)
} }
logger := log.New(file, "", log.LstdFlags) logger := log.New(file, "", log.LstdFlags)
return &Task{Name: name, logFile: file, Logger: logger}, nil taskModel := &model.Task{
ID: taskID,
Name: name,
Type: taskType,
LogFile: logPath,
Status: constant.StatusRunning,
}
taskRepo := repo.NewITaskRepo()
task := &Task{Name: name, logFile: file, Logger: logger, taskRepo: taskRepo, Task: taskModel}
return task, nil
} }
func (t *Task) AddSubTask(name string, action ActionFunc, rollback RollbackFunc) { func (t *Task) AddSubTask(name string, action ActionFunc, rollback RollbackFunc) {
subTask := &SubTask{Name: name, Retry: 0, Timeout: 10 * time.Minute, Action: action, Rollback: rollback} subTask := &SubTask{RootTask: t, Name: name, Retry: 0, Timeout: 10 * time.Minute, Action: action, Rollback: rollback}
t.SubTasks = append(t.SubTasks, subTask) t.SubTasks = append(t.SubTasks, subTask)
} }
func (t *Task) AddSubTaskWithOps(name string, action ActionFunc, rollback RollbackFunc, retry int, timeout time.Duration) { func (t *Task) AddSubTaskWithOps(name string, action ActionFunc, rollback RollbackFunc, retry int, timeout time.Duration) {
subTask := &SubTask{Name: name, Retry: retry, Timeout: timeout, Action: action, Rollback: rollback} subTask := &SubTask{RootTask: t, Name: name, Retry: retry, Timeout: timeout, Action: action, Rollback: rollback}
t.SubTasks = append(t.SubTasks, subTask) t.SubTasks = append(t.SubTasks, subTask)
} }
func (s *SubTask) Execute(logger *log.Logger) bool { func (s *SubTask) Execute() error {
logger.Printf(i18n.GetWithName("SubTaskStart", s.Name)) s.RootTask.Log(s.Name)
var err error
for i := 0; i < s.Retry+1; i++ { for i := 0; i < s.Retry+1; i++ {
if i > 0 { if i > 0 {
logger.Printf(i18n.GetWithName("TaskRetry", strconv.Itoa(i))) s.RootTask.Log(i18n.GetWithName("TaskRetry", strconv.Itoa(i)))
} }
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
defer cancel() defer cancel()
done := make(chan error) done := make(chan error)
go func() { go func() {
done <- s.Action() done <- s.Action(s.RootTask)
}() }()
select { select {
case <-ctx.Done(): case <-ctx.Done():
logger.Printf(i18n.GetWithName("TaskTimeout", s.Name)) s.RootTask.Log(i18n.GetWithName("TaskTimeout", s.Name))
case err := <-done: case err = <-done:
if err != nil { if err != nil {
s.Error = err s.RootTask.Log(i18n.GetWithNameAndErr("SubTaskFailed", s.Name, err))
logger.Printf(i18n.GetWithNameAndErr("TaskFailed", s.Name, err))
} else { } else {
logger.Printf(i18n.GetWithName("TaskSuccess", s.Name)) s.RootTask.Log(i18n.GetWithName("SubTaskSuccess", s.Name))
return true return nil
} }
} }
@ -88,29 +151,77 @@ func (s *SubTask) Execute(logger *log.Logger) bool {
} }
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
} }
if s.Error != nil { return err
s.Error = fmt.Errorf(i18n.GetWithName("TaskFailed", s.Name)) }
}
return false func (t *Task) updateTask(task *model.Task) {
_ = t.taskRepo.Update(context.Background(), task)
} }
func (t *Task) Execute() error { func (t *Task) Execute() error {
t.Logger.Printf(i18n.GetWithName("TaskStart", t.Name)) if err := t.taskRepo.Create(context.Background(), t.Task); err != nil {
return err
}
var err error var err error
t.Log(i18n.GetWithName("TaskStart", t.Name))
for _, subTask := range t.SubTasks { for _, subTask := range t.SubTasks {
if subTask.Execute(t.Logger) { t.Task.CurrentStep = subTask.Name
t.updateTask(t.Task)
if err = subTask.Execute(); err == nil {
if subTask.Rollback != nil { if subTask.Rollback != nil {
t.Rollbacks = append(t.Rollbacks, subTask.Rollback) t.Rollbacks = append(t.Rollbacks, subTask.Rollback)
} }
} else { } else {
err = subTask.Error t.Task.ErrorMsg = err.Error()
t.Task.Status = constant.StatusFailed
for _, rollback := range t.Rollbacks { for _, rollback := range t.Rollbacks {
rollback() rollback()
} }
t.updateTask(t.Task)
break break
} }
} }
t.Logger.Printf(i18n.GetWithName("TaskEnd", t.Name)) if t.Task.Status == constant.Running {
t.Task.Status = constant.StatusSuccess
t.Log(i18n.GetWithName("TaskSuccess", t.Name))
} else {
t.Log(i18n.GetWithName("TaskFailed", t.Name))
}
t.Log("[TASK-END]")
t.Task.EndAt = time.Now()
t.updateTask(t.Task)
_ = t.logFile.Close() _ = t.logFile.Close()
return err return err
} }
func (t *Task) DeleteLogFile() {
_ = os.Remove(t.Task.LogFile)
}
func (t *Task) LogWithStatus(msg string, err error) {
if err != nil {
t.Logger.Printf(i18n.GetWithNameAndErr("FailedStatus", msg, err))
} else {
t.Logger.Printf(i18n.GetWithName("SuccessStatus", msg))
}
}
func (t *Task) Log(msg string) {
t.Logger.Printf(msg)
}
func (t *Task) LogFailed(msg string) {
t.Logger.Printf(msg + i18n.GetMsgByKey("Failed"))
}
func (t *Task) LogFailedWithErr(msg string, err error) {
t.Logger.Printf(fmt.Sprintf("%s %s : %s", msg, i18n.GetMsgByKey("Failed"), err.Error()))
}
func (t *Task) LogSuccess(msg string) {
t.Logger.Printf(msg + i18n.GetMsgByKey("Success"))
}
func (t *Task) LogStart(msg string) {
t.Logger.Printf(fmt.Sprintf("%s%s", i18n.GetMsgByKey("Start"), msg))
}

View File

@ -14,6 +14,7 @@ const (
SyncSuccess = "SyncSuccess" SyncSuccess = "SyncSuccess"
Paused = "Paused" Paused = "Paused"
UpErr = "UpErr" UpErr = "UpErr"
InstallErr = "InstallErr"
ContainerPrefix = "1Panel-" ContainerPrefix = "1Panel-"

View File

@ -11,6 +11,11 @@ const (
TypePhp = "php" TypePhp = "php"
TypeSSL = "ssl" TypeSSL = "ssl"
TypeSystem = "system" TypeSystem = "system"
TypeTask = "task"
TypeImagePull = "image-pull"
TypeImagePush = "image-push"
TypeImageBuild = "image-build"
TypeComposeCreate = "compose-create"
) )
const ( const (

6
agent/constant/task.go Normal file
View File

@ -0,0 +1,6 @@
package constant
const (
TaskInstall = "installApp"
TaskCreateWebsite = "createWebsite"
)

View File

@ -198,10 +198,36 @@ ErrXpackNotActive: "This section is a professional edition feature, please synch
ErrXpackOutOfDate: "The current license has expired, please re-import the license in Panel Settings-License interface" ErrXpackOutOfDate: "The current license has expired, please re-import the license in Panel Settings-License interface"
#task #task
TaskStart: "{{.name}} started [START]" TaskStart: "{{.name}} Start [START]"
TaskEnd: "{{.name}} ended [COMPLETED]" TaskEnd: "{{.name}} End [COMPLETED]"
TaskFailed: "{{.name}} failed: {{.err}}" TaskFailed: "{{.name}} Failed"
TaskTimeout: "{{.name}} timed out" TaskTimeout: "{{.name}} Timeout"
TaskSuccess: "{{.name}} succeeded" TaskSuccess: "{{.name}} Success"
TaskRetry: "Start {{.name}} retry" TaskRetry: "Starting {{.name}} Retry"
SubTaskStart: "Start {{.name}}" SubTaskSuccess: "{{ .name }} Success"
SubTaskFailed: "{{ .name }} Failed: {{ .err }}"
TaskInstall: "Install"
TaskUninstall: "Uninstall"
TaskCreate: "Create"
TaskDelete: "Delete"
TaskUpgrade: "Upgrade"
TaskUpdate: "Update"
TaskRestart: "Restart"
Website: "Website"
App: "App"
Runtime: "Runtime"
Database: "Database"
ConfigFTP: "Create FTP User {{ .name }}"
ConfigOpenresty: "Create Openresty Configuration File"
InstallAppSuccess: "App {{ .name }} Installed Successfully"
ConfigRuntime: "Configure Runtime"
ConfigApp: "Configure App"
SuccessStatus: "{{ .name }} Success"
FailedStatus: "{{ .name }} Failed {{.err}}"
HandleLink: "Handle App Link"
HandleDatabaseApp: "Handle App Parameters"
ExecShell: "Execute {{ .name }} Script"
PullImage: "Pull Image"
Start: "Start"
Run: "Run"

View File

@ -202,8 +202,33 @@ ErrXpackOutOfDate: "當前許可證已過期,請重新在 面板設置-許可
#task #task
TaskStart: "{{.name}} 開始 [START]" TaskStart: "{{.name}} 開始 [START]"
TaskEnd: "{{.name}} 結束 [COMPLETED]" TaskEnd: "{{.name}} 結束 [COMPLETED]"
TaskFailed: "{{.name}} 失敗: {{.err}}" TaskFailed: "{{.name}} 失敗"
TaskTimeout: "{{.name}} 時" TaskTimeout: "{{.name}} 時"
TaskSuccess: "{{.name}} 成功" TaskSuccess: "{{.name}} 成功"
TaskRetry: "開始第 {{.name}} 次重試" TaskRetry: "開始第 {{.name}} 次重試"
SubTaskStart: "開始 {{.name}}" SubTaskSuccess: "{{ .name }} 成功"
SubTaskFailed: "{{ .name }} 失敗: {{ .err }}"
TaskInstall: "安裝"
TaskUninstall: "卸載"
TaskCreate: "創建"
TaskDelete: "刪除"
TaskUpgrade: "升級"
TaskUpdate: "更新"
TaskRestart: "重啟"
Website: "網站"
App: "應用"
Runtime: "運行環境"
Database: "數據庫"
ConfigFTP: "創建 FTP 用戶 {{ .name }}"
ConfigOpenresty: "創建 Openresty 配置文件"
InstallAppSuccess: "應用 {{ .name }} 安裝成功"
ConfigRuntime: "配置運行環境"
ConfigApp: "配置應用"
SuccessStatus: "{{ .name }} 成功"
FailedStatus: "{{ .name }} 失敗 {{.err}}"
HandleLink: "處理應用關聯"
HandleDatabaseApp: "處理應用參數"
ExecShell: "執行 {{ .name }} 腳本"
PullImage: "拉取鏡像"
Start: "開始"
Run: "啟動"

View File

@ -203,8 +203,33 @@ ErrXpackOutOfDate: "当前许可证已过期,请重新在 面板设置-许可
#task #task
TaskStart: "{{.name}} 开始 [START]" TaskStart: "{{.name}} 开始 [START]"
TaskEnd: "{{.name}} 结束 [COMPLETED]" TaskEnd: "{{.name}} 结束 [COMPLETED]"
TaskFailed: "{{.name}} 失败: {{.err}}" TaskFailed: "{{.name}} 失败"
TaskTimeout: "{{.name}} 超时" TaskTimeout: "{{.name}} 超时"
TaskSuccess: "{{.name}} 成功" TaskSuccess: "{{.name}} 成功"
TaskRetry: "开始第 {{.name}} 次重试" TaskRetry: "开始第 {{.name}} 次重试"
SubTaskStart: "开始 {{.name}}" SubTaskSuccess: "{{ .name }} 成功"
SubTaskFailed: "{{ .name }} 失败: {{ .err }}"
TaskInstall: "安装"
TaskUninstall: "卸载"
TaskCreate: "创建"
TaskDelete: "删除"
TaskUpgrade: "升级"
TaskUpdate: "更新"
TaskRestart: "重启"
Website: "网站"
App: "应用"
Runtime: "运行环境"
Database: "数据库"
ConfigFTP: "创建 FTP 用户 {{ .name }}"
ConfigOpenresty: "创建 Openresty 配置文件"
InstallAppSuccess: "应用 {{ .name }} 安装成功"
ConfigRuntime: "配置运行环境"
ConfigApp: "配置应用"
SuccessStatus: "{{ .name }} 成功"
FailedStatus: "{{ .name }} 失败 {{.err}}"
HandleLink: "处理应用关联"
HandleDatabaseApp: "处理应用参数"
ExecShell: "执行 {{ .name }} 脚本"
PullImage: "拉取镜像"
Start: "开始"
Run: "启动"

View File

@ -17,6 +17,7 @@ func Init() {
migrations.InitDefaultGroup, migrations.InitDefaultGroup,
migrations.InitDefaultCA, migrations.InitDefaultCA,
migrations.InitPHPExtensions, migrations.InitPHPExtensions,
migrations.AddTask,
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
global.LOG.Error(err) global.LOG.Error(err)

View File

@ -253,3 +253,11 @@ var InitPHPExtensions = &gormigrate.Migration{
return nil return nil
}, },
} }
var AddTask = &gormigrate.Migration{
ID: "20240724-add-task",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(
&model.Task{})
},
}

View File

@ -6,7 +6,6 @@ import (
"os" "os"
"github.com/1Panel-dev/1Panel/agent/cron" "github.com/1Panel-dev/1Panel/agent/cron"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n" "github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/init/app" "github.com/1Panel-dev/1Panel/agent/init/app"
"github.com/1Panel-dev/1Panel/agent/init/business" "github.com/1Panel-dev/1Panel/agent/init/business"
@ -40,25 +39,23 @@ func Start() {
server := &http.Server{ server := &http.Server{
Handler: rootRouter, Handler: rootRouter,
} }
if len(global.CurrentNode) == 0 || global.CurrentNode == "127.0.0.1" { //ln, err := net.Listen("tcp4", "0.0.0.0:9998")
_ = os.Remove("/tmp/agent.sock") //if err != nil {
// panic(err)
//}
//type tcpKeepAliveListener struct {
// *net.TCPListener
//}
//
//global.LOG.Info("listen at http://0.0.0.0:9998")
//if err := server.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}); err != nil {
// panic(err)
//}
os.Remove("/tmp/agent.sock")
listener, err := net.Listen("unix", "/tmp/agent.sock") listener, err := net.Listen("unix", "/tmp/agent.sock")
if err != nil { if err != nil {
panic(err) panic(err)
} }
_ = server.Serve(listener) server.Serve(listener)
} else {
server.Addr = "0.0.0.0:9999"
type tcpKeepAliveListener struct {
*net.TCPListener
}
ln, err := net.Listen("tcp4", "0.0.0.0:9999")
if err != nil {
panic(err)
}
global.LOG.Info("listen at http://0.0.0.0:9999")
if err := server.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}); err != nil {
panic(err)
}
}
} }

View File

@ -26,7 +26,7 @@ type ILogService interface {
CreateLoginLog(operation model.LoginLog) error CreateLoginLog(operation model.LoginLog) error
PageLoginLog(search dto.SearchLgLogWithPage) (int64, interface{}, error) PageLoginLog(search dto.SearchLgLogWithPage) (int64, interface{}, error)
CreateOperationLog(operation model.OperationLog) error CreateOperationLog(operation *model.OperationLog) error
PageOperationLog(search dto.SearchOpLogWithPage) (int64, interface{}, error) PageOperationLog(search dto.SearchOpLogWithPage) (int64, interface{}, error)
CleanLogs(logtype string) error CleanLogs(logtype string) error
@ -92,8 +92,8 @@ func (u *LogService) PageLoginLog(req dto.SearchLgLogWithPage) (int64, interface
return total, dtoOps, err return total, dtoOps, err
} }
func (u *LogService) CreateOperationLog(operation model.OperationLog) error { func (u *LogService) CreateOperationLog(operation *model.OperationLog) error {
return logRepo.CreateOperationLog(&operation) return logRepo.CreateOperationLog(operation)
} }
func (u *LogService) PageOperationLog(req dto.SearchOpLogWithPage) (int64, interface{}, error) { func (u *LogService) PageOperationLog(req dto.SearchOpLogWithPage) (int64, interface{}, error) {

View File

@ -27,7 +27,7 @@ func OperationLog() gin.HandlerFunc {
} }
source := loadLogInfo(c.Request.URL.Path) source := loadLogInfo(c.Request.URL.Path)
record := model.OperationLog{ record := &model.OperationLog{
Source: source, Source: source,
IP: c.ClientIP(), IP: c.ClientIP(),
Method: strings.ToLower(c.Request.Method), Method: strings.ToLower(c.Request.Method),

View File

@ -22,7 +22,7 @@ func Proxy() gin.HandlerFunc {
return return
} }
currentNode := c.Request.Header.Get("CurrentNode") currentNode := c.Request.Header.Get("CurrentNode")
if currentNode == "127.0.0.1" { if len(currentNode) == 0 || currentNode == "127.0.0.1" {
sockPath := "/tmp/agent.sock" sockPath := "/tmp/agent.sock"
if _, err := os.Stat(sockPath); err != nil { if _, err := os.Stat(sockPath); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrProxy, err) helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrProxy, err)

View File

@ -49,6 +49,7 @@
"qs": "^6.12.1", "qs": "^6.12.1",
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"unplugin-vue-define-options": "^0.7.3", "unplugin-vue-define-options": "^0.7.3",
"uuid": "^10.0.0",
"vue": "^3.4.27", "vue": "^3.4.27",
"vue-clipboard3": "^2.0.0", "vue-clipboard3": "^2.0.0",
"vue-codemirror": "^6.1.1", "vue-codemirror": "^6.1.1",

View File

@ -174,6 +174,7 @@ export namespace File {
name?: string; name?: string;
page: number; page: number;
pageSize: number; pageSize: number;
taskID?: string;
} }
export interface Favorite extends CommonModel { export interface Favorite extends CommonModel {

View File

@ -79,6 +79,7 @@ export namespace Website {
proxyType: string; proxyType: string;
ftpUser: string; ftpUser: string;
ftpPassword: string; ftpPassword: string;
taskID: string;
} }
export interface WebSiteUpdateReq { export interface WebSiteUpdateReq {

View File

@ -19,7 +19,7 @@ export const CreateWebsite = (req: Website.WebSiteCreateReq) => {
if (request.ftpPassword) { if (request.ftpPassword) {
request.ftpPassword = Base64.encode(request.ftpPassword); request.ftpPassword = Base64.encode(request.ftpPassword);
} }
return http.post<any>(`/websites`, request); return http.post<any>(`/websites`, request, TimeoutEnum.T_10M);
}; };
export const OpWebsite = (req: Website.WebSiteOp) => { export const OpWebsite = (req: Website.WebSiteOp) => {

View File

@ -246,7 +246,7 @@ const initCodemirror = () => {
} }
}); });
let hljsDom = scrollerElement.value.querySelector('.hljs') as HTMLElement; let hljsDom = scrollerElement.value.querySelector('.hljs') as HTMLElement;
hljsDom.style['min-height'] = '500px'; hljsDom.style['min-height'] = '100px';
} }
}); });
}; };

View File

@ -0,0 +1,212 @@
<template>
<el-dialog
v-model="open"
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="showClose"
:before-close="handleClose"
class="task-log-dialog"
>
<div>
<highlightjs ref="editorRef" language="JavaScript" :autodetect="false" :code="content"></highlightjs>
</div>
</el-dialog>
</template>
<script lang="ts" setup>
import { nextTick, onUnmounted, reactive, ref } from 'vue';
import { ReadByLine } from '@/api/modules/files';
const editorRef = ref();
const data = ref({
enable: false,
content: '',
path: '',
});
let timer: NodeJS.Timer | null = null;
const tailLog = ref(false);
const content = ref('');
const end = ref(false);
const lastContent = ref('');
const scrollerElement = ref<HTMLElement | null>(null);
const minPage = ref(1);
const maxPage = ref(1);
const open = ref(false);
const taskID = ref('');
const showClose = ref(false);
const readReq = reactive({
taskID: '',
type: 'task',
page: 1,
pageSize: 500,
latest: false,
});
const stopSignals = ['[TASK-END]'];
const acceptParams = (id: string, closeShow: boolean) => {
if (closeShow) {
showClose.value = closeShow;
}
taskID.value = id;
open.value = true;
initCodemirror();
init();
};
const getContent = (pre: boolean) => {
readReq.taskID = taskID.value;
if (readReq.page < 1) {
readReq.page = 1;
}
ReadByLine(readReq).then((res) => {
if (!end.value && res.data.end) {
lastContent.value = content.value;
}
res.data.content = res.data.content.replace(/\\u(\w{4})/g, function (match, grp) {
return String.fromCharCode(parseInt(grp, 16));
});
data.value = res.data;
if (res.data.content != '') {
if (stopSignals.some((signal) => res.data.content.endsWith(signal))) {
onCloseLog();
}
if (end.value) {
if (lastContent.value == '') {
content.value = res.data.content;
} else {
content.value = pre
? res.data.content + '\n' + lastContent.value
: lastContent.value + '\n' + res.data.content;
}
} else {
if (content.value == '') {
content.value = res.data.content;
} else {
content.value = pre
? res.data.content + '\n' + content.value
: content.value + '\n' + res.data.content;
}
}
}
end.value = res.data.end;
nextTick(() => {
if (pre) {
if (scrollerElement.value.scrollHeight > 2000) {
scrollerElement.value.scrollTop = 2000;
}
} else {
scrollerElement.value.scrollTop = scrollerElement.value.scrollHeight;
}
});
if (readReq.latest) {
readReq.page = res.data.total;
readReq.latest = false;
maxPage.value = res.data.total;
minPage.value = res.data.total;
}
});
};
const changeTail = (fromOutSide: boolean) => {
if (fromOutSide) {
tailLog.value = !tailLog.value;
}
if (tailLog.value) {
timer = setInterval(() => {
getContent(false);
}, 1000 * 3);
} else {
onCloseLog();
}
};
const handleClose = () => {
onCloseLog();
open.value = false;
};
const onCloseLog = async () => {
tailLog.value = false;
clearInterval(Number(timer));
timer = null;
};
function isScrolledToBottom(element: HTMLElement): boolean {
return element.scrollTop + element.clientHeight + 1 >= element.scrollHeight;
}
function isScrolledToTop(element: HTMLElement): boolean {
return element.scrollTop === 0;
}
const init = () => {
tailLog.value = true;
if (tailLog.value) {
changeTail(false);
}
readReq.latest = true;
getContent(false);
};
const initCodemirror = () => {
nextTick(() => {
if (editorRef.value) {
scrollerElement.value = editorRef.value.$el as HTMLElement;
scrollerElement.value.addEventListener('scroll', function () {
if (isScrolledToBottom(scrollerElement.value)) {
readReq.page = maxPage.value;
getContent(false);
}
if (isScrolledToTop(scrollerElement.value)) {
readReq.page = minPage.value - 1;
if (readReq.page < 1) {
return;
}
minPage.value = readReq.page;
getContent(true);
}
});
let hljsDom = scrollerElement.value.querySelector('.hljs') as HTMLElement;
hljsDom.style['min-height'] = '100px';
}
});
};
onUnmounted(() => {
onCloseLog();
});
defineExpose({ acceptParams, handleClose });
</script>
<style lang="scss" scoped>
.task-log-dialog {
--dialog-max-height: 80vh;
--dialog-header-height: 50px;
--dialog-padding: 20px;
.el-dialog {
max-width: 60%;
max-height: var(--dialog-max-height);
margin-top: 5vh !important;
display: flex;
flex-direction: column;
}
.el-dialog__body {
flex: 1;
overflow: hidden;
padding: var(--dialog-padding);
}
.log-container {
height: calc(var(--dialog-max-height) - var(--dialog-header-height) - var(--dialog-padding) * 2);
overflow: hidden;
}
.log-file {
height: 100%;
}
}
</style>

View File

@ -42,7 +42,7 @@ const GlobalStore = defineStore({
errStatus: '', errStatus: '',
currentNode: '', currentNode: '127.0.0.1',
}), }),
getters: { getters: {
isDarkTheme: (state) => isDarkTheme: (state) =>

View File

@ -40,7 +40,6 @@ import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules'; import { Rules } from '@/global/form-rules';
import i18n from '@/lang'; import i18n from '@/lang';
import { ElForm } from 'element-plus'; import { ElForm } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { Host } from '@/api/interface/host'; import { Host } from '@/api/interface/host';
import { operateForwardRule } from '@/api/modules/host'; import { operateForwardRule } from '@/api/modules/host';

View File

@ -163,7 +163,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onBeforeUnmount, reactive, ref, shallowRef } from 'vue'; import { onBeforeUnmount, reactive, ref } from 'vue';
import i18n from '@/lang'; import i18n from '@/lang';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';

View File

@ -13,10 +13,6 @@
<el-button type="primary" @click="openCreate"> <el-button type="primary" @click="openCreate">
{{ $t('runtime.create') }} {{ $t('runtime.create') }}
</el-button> </el-button>
<el-button type="primary" plain @click="onOpenBuildCache()">
{{ $t('container.cleanBuildCache') }}
</el-button>
</template> </template>
<template #main> <template #main>
<ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()" :heightDiff="350"> <ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()" :heightDiff="350">
@ -115,8 +111,6 @@ import { Promotion } from '@element-plus/icons-vue';
import PortJumpDialog from '@/components/port-jump/index.vue'; import PortJumpDialog from '@/components/port-jump/index.vue';
import AppResources from '@/views/website/runtime/php/check/index.vue'; import AppResources from '@/views/website/runtime/php/check/index.vue';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { containerPrune } from '@/api/modules/container';
import { MsgSuccess } from '@/utils/message';
let timer: NodeJS.Timer | null = null; let timer: NodeJS.Timer | null = null;
const loading = ref(false); const loading = ref(false);
@ -221,29 +215,6 @@ const openDelete = async (row: Runtime.Runtime) => {
}); });
}; };
const onOpenBuildCache = () => {
ElMessageBox.confirm(i18n.global.t('container.delBuildCacheHelper'), i18n.global.t('container.cleanBuildCache'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
let params = {
pruneType: 'buildcache',
withTagAll: false,
};
await containerPrune(params)
.then((res) => {
loading.value = false;
MsgSuccess(i18n.global.t('container.cleanSuccess', [res.data.deletedNumber]));
search();
})
.catch(() => {
loading.value = false;
});
});
};
const openLog = (row: any) => { const openLog = (row: any) => {
composeLogRef.value.acceptParams({ compose: row.path + '/docker-compose.yml', resource: row.name }); composeLogRef.value.acceptParams({ compose: row.path + '/docker-compose.yml', resource: row.name });
}; };

View File

@ -13,10 +13,6 @@
<el-button type="primary" @click="openCreate"> <el-button type="primary" @click="openCreate">
{{ $t('runtime.create') }} {{ $t('runtime.create') }}
</el-button> </el-button>
<el-button type="primary" plain @click="onOpenBuildCache()">
{{ $t('container.cleanBuildCache') }}
</el-button>
</template> </template>
<template #main> <template #main>
<ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()" :heightDiff="350"> <ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()" :heightDiff="350">
@ -115,8 +111,6 @@ import { Promotion } from '@element-plus/icons-vue';
import PortJumpDialog from '@/components/port-jump/index.vue'; import PortJumpDialog from '@/components/port-jump/index.vue';
import AppResources from '@/views/website/runtime/php/check/index.vue'; import AppResources from '@/views/website/runtime/php/check/index.vue';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { containerPrune } from '@/api/modules/container';
import { MsgSuccess } from '@/utils/message';
let timer: NodeJS.Timer | null = null; let timer: NodeJS.Timer | null = null;
const loading = ref(false); const loading = ref(false);
@ -221,29 +215,6 @@ const openDelete = (row: Runtime.Runtime) => {
}); });
}; };
const onOpenBuildCache = () => {
ElMessageBox.confirm(i18n.global.t('container.delBuildCacheHelper'), i18n.global.t('container.cleanBuildCache'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
let params = {
pruneType: 'buildcache',
withTagAll: false,
};
await containerPrune(params)
.then((res) => {
loading.value = false;
MsgSuccess(i18n.global.t('container.cleanSuccess', [res.data.deletedNumber]));
search();
})
.catch(() => {
loading.value = false;
});
});
};
const openLog = (row: any) => { const openLog = (row: any) => {
composeLogRef.value.acceptParams({ compose: row.path + '/docker-compose.yml', resource: row.name }); composeLogRef.value.acceptParams({ compose: row.path + '/docker-compose.yml', resource: row.name });
}; };

View File

@ -13,10 +13,6 @@
<el-button type="primary" @click="openCreate"> <el-button type="primary" @click="openCreate">
{{ $t('runtime.create') }} {{ $t('runtime.create') }}
</el-button> </el-button>
<el-button type="primary" plain @click="onOpenBuildCache()">
{{ $t('container.cleanBuildCache') }}
</el-button>
</template> </template>
<template #main> <template #main>
<ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()" :heightDiff="350"> <ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()" :heightDiff="350">
@ -117,8 +113,6 @@ import { Promotion } from '@element-plus/icons-vue';
import PortJumpDialog from '@/components/port-jump/index.vue'; import PortJumpDialog from '@/components/port-jump/index.vue';
import AppResources from '@/views/website/runtime/php/check/index.vue'; import AppResources from '@/views/website/runtime/php/check/index.vue';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { containerPrune } from '@/api/modules/container';
import { MsgSuccess } from '@/utils/message';
let timer: NodeJS.Timer | null = null; let timer: NodeJS.Timer | null = null;
const loading = ref(false); const loading = ref(false);
@ -237,29 +231,6 @@ const openDelete = async (row: Runtime.Runtime) => {
}); });
}; };
const onOpenBuildCache = () => {
ElMessageBox.confirm(i18n.global.t('container.delBuildCacheHelper'), i18n.global.t('container.cleanBuildCache'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
let params = {
pruneType: 'buildcache',
withTagAll: false,
};
await containerPrune(params)
.then((res) => {
loading.value = false;
MsgSuccess(i18n.global.t('container.cleanSuccess', [res.data.deletedNumber]));
search();
})
.catch(() => {
loading.value = false;
});
});
};
const openLog = (row: any) => { const openLog = (row: any) => {
composeLogRef.value.acceptParams({ compose: row.path + '/docker-compose.yml', resource: row.name }); composeLogRef.value.acceptParams({ compose: row.path + '/docker-compose.yml', resource: row.name });
}; };

View File

@ -346,6 +346,7 @@
{{ $t('runtime.openrestyWarn') }} {{ $t('runtime.openrestyWarn') }}
</span> </span>
</el-card> </el-card>
<TaskLog ref="taskLog" />
</DrawerPro> </DrawerPro>
</template> </template>
@ -365,6 +366,8 @@ import { Group } from '@/api/interface/group';
import { SearchRuntimes } from '@/api/modules/runtime'; import { SearchRuntimes } from '@/api/modules/runtime';
import { Runtime } from '@/api/interface/runtime'; import { Runtime } from '@/api/interface/runtime';
import { getRandomStr } from '@/utils/util'; import { getRandomStr } from '@/utils/util';
import TaskLog from '@/components/task-log/index.vue';
import { v4 as uuidv4 } from 'uuid';
const websiteForm = ref<FormInstance>(); const websiteForm = ref<FormInstance>();
const website = ref({ const website = ref({
@ -402,6 +405,7 @@ const website = ref({
proxyProtocol: 'http://', proxyProtocol: 'http://',
proxyAddress: '', proxyAddress: '',
runtimeType: 'php', runtimeType: 'php',
taskID: '',
}); });
const rules = ref<any>({ const rules = ref<any>({
primaryDomain: [Rules.domainWithPort], primaryDomain: [Rules.domainWithPort],
@ -453,6 +457,7 @@ const runtimeReq = ref<Runtime.RuntimeReq>({
const runtimes = ref<Runtime.RuntimeDTO[]>([]); const runtimes = ref<Runtime.RuntimeDTO[]>([]);
const versionExist = ref(true); const versionExist = ref(true);
const em = defineEmits(['close']); const em = defineEmits(['close']);
const taskLog = ref();
const handleClose = () => { const handleClose = () => {
open.value = false; open.value = false;
@ -612,6 +617,10 @@ function isSubsetOfStrArray(primaryDomain: string, otherDomains: string): boolea
return true; return true;
} }
const openTaskLog = (taskID: string) => {
taskLog.value.acceptParams(taskID);
};
const submit = async (formEl: FormInstance | undefined) => { const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;
await formEl.validate((valid) => { await formEl.validate((valid) => {
@ -638,6 +647,8 @@ const submit = async (formEl: FormInstance | undefined) => {
website.value.ftpUser = ''; website.value.ftpUser = '';
website.value.ftpPassword = ''; website.value.ftpPassword = '';
} }
const taskID = uuidv4();
website.value.taskID = taskID;
CreateWebsite(website.value) CreateWebsite(website.value)
.then(() => { .then(() => {
MsgSuccess(i18n.global.t('commons.msg.createSuccess')); MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
@ -646,6 +657,7 @@ const submit = async (formEl: FormInstance | undefined) => {
.finally(() => { .finally(() => {
loading.value = false; loading.value = false;
}); });
openTaskLog(taskID);
} }
}) })
.catch(() => { .catch(() => {