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

pull/5993/head
zhengkunwang223 4 months ago
parent 865b6cba3f
commit 0e5d6e825e

1
.gitignore vendored

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

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

@ -1,8 +1,6 @@
package v1
import (
"encoding/base64"
"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/request"
@ -78,14 +76,6 @@ func (b *BaseApi) CreateWebsite(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil {
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)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)

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

@ -30,7 +30,8 @@ type WebsiteCreate struct {
FtpUser string `json:"ftpUser"`
FtpPassword string `json:"ftpPassword"`
RuntimeID uint `json:"runtimeID"`
RuntimeID uint `json:"runtimeID"`
TaskID string `json:"taskID"`
RuntimeConfig
}

@ -0,0 +1 @@
package response

@ -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"`
}

@ -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
}

@ -5,18 +5,12 @@ import (
"encoding/base64"
"encoding/json"
"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/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model"
"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/constant"
"github.com/1Panel-dev/1Panel/agent/global"
@ -27,7 +21,14 @@ import (
http2 "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/google/uuid"
"gopkg.in/yaml.v3"
"net/http"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
)
type AppService struct {
@ -38,7 +39,7 @@ type IAppService interface {
GetAppTags() ([]response.TagDTO, error)
GetApp(key string) (*response.AppDTO, 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
GetAppUpdate() (*response.AppUpdateRes, error)
GetAppDetailByID(id uint) (*response.AppDetailDTO, error)
@ -295,7 +296,7 @@ func (a AppService) GetIgnoredApp() ([]response.IgnoredApp, error) {
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 {
err = buserr.WithDetail(constant.Err1PanelNetworkFailed, err.Error(), nil)
return
@ -423,14 +424,6 @@ func (a AppService) Install(ctx context.Context, req request.AppInstallCreate) (
}
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 {
database, _ := databaseRepo.Get(commonRepo.WithByName(hostName.(string)))
if !reflect.DeepEqual(database, model.Database{}) {
@ -445,29 +438,48 @@ func (a AppService) Install(ctx context.Context, req request.AppInstallCreate) (
}
appInstall.Env = string(paramByte)
if err = appInstallRepo.Create(ctx, appInstall); err != nil {
if err = appInstallRepo.Create(context.Background(), appInstall); err != nil {
return
}
if err = createLink(ctx, app, appInstall, req.Params); err != nil {
taskID := uuid.New().String()
installTask, err := task.NewTaskWithOps(appInstall.Name, task.TaskCreate, task.TaskScopeApp, taskID)
if err != nil {
return
}
go func() {
defer func() {
if err != nil {
appInstall.Status = constant.UpErr
appInstall.Message = err.Error()
_ = appInstallRepo.Save(context.Background(), appInstall)
}
}()
if err = copyData(app, appDetail, appInstall, req); 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(appInstall, "init"); err != nil {
return
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.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)
}
upApp(appInstall, req.PullImage)
}()
go updateToolApp(appInstall)
return
}

@ -5,6 +5,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/task"
"math"
"net/http"
"os"
@ -130,78 +131,82 @@ var ToolKeys = map[string]uint{
"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
if DatabaseKeys[app.Key] > 0 {
database := &model.Database{
AppInstallID: appInstall.ID,
Name: appInstall.Name,
Type: app.Key,
Version: appInstall.Version,
From: "local",
Address: appInstall.ServiceName,
Port: DatabaseKeys[app.Key],
}
detail, err := appDetailRepo.GetFirst(commonRepo.WithByID(appInstall.AppDetailId))
if err != nil {
return err
}
formFields := &dto.AppForm{}
if err := json.Unmarshal([]byte(detail.Params), formFields); err != nil {
return err
}
for _, form := range formFields.FormFields {
if form.EnvKey == "PANEL_APP_PORT_HTTP" {
portFloat, ok := form.Default.(float64)
if ok {
database.Port = uint(int(portFloat))
}
break
handleDataBaseApp := func(task *task.Task) error {
database := &model.Database{
AppInstallID: appInstall.ID,
Name: appInstall.Name,
Type: app.Key,
Version: appInstall.Version,
From: "local",
Address: appInstall.ServiceName,
Port: DatabaseKeys[app.Key],
}
detail, err := appDetailRepo.GetFirst(commonRepo.WithByID(appInstall.AppDetailId))
if err != nil {
return err
}
}
switch app.Key {
case constant.AppMysql, constant.AppMariaDB, constant.AppPostgresql, constant.AppMongodb:
if password, ok := params["PANEL_DB_ROOT_PASSWORD"]; ok {
if password != "" {
database.Password = password.(string)
if app.Key == "mysql" || app.Key == "mariadb" {
database.Username = "root"
}
if rootUser, ok := params["PANEL_DB_ROOT_USER"]; ok {
database.Username = rootUser.(string)
}
authParam := dto.AuthParam{
RootPassword: password.(string),
RootUser: database.Username,
}
authByte, err := json.Marshal(authParam)
if err != nil {
return err
formFields := &dto.AppForm{}
if err := json.Unmarshal([]byte(detail.Params), formFields); err != nil {
return err
}
for _, form := range formFields.FormFields {
if form.EnvKey == "PANEL_APP_PORT_HTTP" {
portFloat, ok := form.Default.(float64)
if ok {
database.Port = uint(int(portFloat))
}
appInstall.Param = string(authByte)
break
}
}
case constant.AppRedis:
if password, ok := params["PANEL_REDIS_ROOT_PASSWORD"]; ok {
if password != "" {
authParam := dto.RedisAuthParam{
RootPassword: password.(string),
switch app.Key {
case constant.AppMysql, constant.AppMariaDB, constant.AppPostgresql, constant.AppMongodb:
if password, ok := params["PANEL_DB_ROOT_PASSWORD"]; ok {
if password != "" {
database.Password = password.(string)
if app.Key == "mysql" || app.Key == "mariadb" {
database.Username = "root"
}
if rootUser, ok := params["PANEL_DB_ROOT_USER"]; ok {
database.Username = rootUser.(string)
}
authParam := dto.AuthParam{
RootPassword: password.(string),
RootUser: database.Username,
}
authByte, err := json.Marshal(authParam)
if err != nil {
return err
}
appInstall.Param = string(authByte)
}
authByte, err := json.Marshal(authParam)
if err != nil {
return err
}
case constant.AppRedis:
if password, ok := params["PANEL_REDIS_ROOT_PASSWORD"]; ok {
if password != "" {
authParam := dto.RedisAuthParam{
RootPassword: password.(string),
}
authByte, err := json.Marshal(authParam)
if err != nil {
return err
}
appInstall.Param = string(authByte)
}
appInstall.Param = string(authByte)
database.Password = password.(string)
}
database.Password = password.(string)
}
return databaseRepo.Create(ctx, database)
}
if err := databaseRepo.Create(ctx, database); err != nil {
return err
}
installTask.AddSubTask(i18n.GetMsgByKey("HandleDatabaseApp"), handleDataBaseApp, deleteAppLink)
}
if ToolKeys[app.Key] > 0 {
if app.Key == "minio" {
@ -231,95 +236,79 @@ func createLink(ctx context.Context, app model.App, appInstall *model.AppInstall
}
if !reflect.DeepEqual(dbConfig, dto.AppDatabase{}) && dbConfig.ServiceName != "" {
hostName := params["PANEL_DB_HOST_NAME"]
if hostName == nil || hostName.(string) == "" {
return nil
}
database, _ := databaseRepo.Get(commonRepo.WithByName(hostName.(string)))
if database.ID == 0 {
return nil
}
var resourceId uint
if dbConfig.DbName != "" && dbConfig.DbUser != "" && dbConfig.Password != "" {
switch database.Type {
case constant.AppPostgresql, constant.AppPostgres:
iPostgresqlRepo := repo.NewIPostgresqlRepo()
oldPostgresqlDb, _ := iPostgresqlRepo.Get(commonRepo.WithByName(dbConfig.DbName), iPostgresqlRepo.WithByFrom(constant.ResourceLocal))
resourceId = oldPostgresqlDb.ID
if oldPostgresqlDb.ID > 0 {
if oldPostgresqlDb.Username != dbConfig.DbUser || oldPostgresqlDb.Password != dbConfig.Password {
return buserr.New(constant.ErrDbUserNotValid)
}
} else {
var createPostgresql dto.PostgresqlDBCreate
createPostgresql.Name = dbConfig.DbName
createPostgresql.Username = dbConfig.DbUser
createPostgresql.Database = database.Name
createPostgresql.Format = "UTF8"
createPostgresql.Password = dbConfig.Password
createPostgresql.From = database.From
createPostgresql.SuperUser = true
pgdb, err := NewIPostgresqlService().Create(ctx, createPostgresql)
if err != nil {
return err
}
resourceId = pgdb.ID
}
case constant.AppMysql, constant.AppMariaDB:
iMysqlRepo := repo.NewIMysqlRepo()
oldMysqlDb, _ := iMysqlRepo.Get(commonRepo.WithByName(dbConfig.DbName), iMysqlRepo.WithByFrom(constant.ResourceLocal))
resourceId = oldMysqlDb.ID
if oldMysqlDb.ID > 0 {
if oldMysqlDb.Username != dbConfig.DbUser || oldMysqlDb.Password != dbConfig.Password {
return buserr.New(constant.ErrDbUserNotValid)
createAppDataBase := func(rootTask *task.Task) error {
hostName := params["PANEL_DB_HOST_NAME"]
if hostName == nil || hostName.(string) == "" {
return nil
}
database, _ := databaseRepo.Get(commonRepo.WithByName(hostName.(string)))
if database.ID == 0 {
return nil
}
var resourceId uint
if dbConfig.DbName != "" && dbConfig.DbUser != "" && dbConfig.Password != "" {
switch database.Type {
case constant.AppPostgresql, constant.AppPostgres:
iPostgresqlRepo := repo.NewIPostgresqlRepo()
oldPostgresqlDb, _ := iPostgresqlRepo.Get(commonRepo.WithByName(dbConfig.DbName), iPostgresqlRepo.WithByFrom(constant.ResourceLocal))
resourceId = oldPostgresqlDb.ID
if oldPostgresqlDb.ID > 0 {
if oldPostgresqlDb.Username != dbConfig.DbUser || oldPostgresqlDb.Password != dbConfig.Password {
return buserr.New(constant.ErrDbUserNotValid)
}
} else {
var createPostgresql dto.PostgresqlDBCreate
createPostgresql.Name = dbConfig.DbName
createPostgresql.Username = dbConfig.DbUser
createPostgresql.Database = database.Name
createPostgresql.Format = "UTF8"
createPostgresql.Password = dbConfig.Password
createPostgresql.From = database.From
createPostgresql.SuperUser = true
pgdb, err := NewIPostgresqlService().Create(ctx, createPostgresql)
if err != nil {
return err
}
resourceId = pgdb.ID
}
} else {
var createMysql dto.MysqlDBCreate
createMysql.Name = dbConfig.DbName
createMysql.Username = dbConfig.DbUser
createMysql.Database = database.Name
createMysql.Format = "utf8mb4"
createMysql.Permission = "%"
createMysql.Password = dbConfig.Password
createMysql.From = database.From
mysqldb, err := NewIMysqlService().Create(ctx, createMysql)
if err != nil {
return err
case constant.AppMysql, constant.AppMariaDB:
iMysqlRepo := repo.NewIMysqlRepo()
oldMysqlDb, _ := iMysqlRepo.Get(commonRepo.WithByName(dbConfig.DbName), iMysqlRepo.WithByFrom(constant.ResourceLocal))
resourceId = oldMysqlDb.ID
if oldMysqlDb.ID > 0 {
if oldMysqlDb.Username != dbConfig.DbUser || oldMysqlDb.Password != dbConfig.Password {
return buserr.New(constant.ErrDbUserNotValid)
}
} else {
var createMysql dto.MysqlDBCreate
createMysql.Name = dbConfig.DbName
createMysql.Username = dbConfig.DbUser
createMysql.Database = database.Name
createMysql.Format = "utf8mb4"
createMysql.Permission = "%"
createMysql.Password = dbConfig.Password
createMysql.From = database.From
mysqldb, err := NewIMysqlService().Create(ctx, createMysql)
if err != nil {
return err
}
resourceId = mysqldb.ID
}
resourceId = mysqldb.ID
}
}
}
var installResource model.AppInstallResource
installResource.ResourceId = resourceId
installResource.AppInstallId = appInstall.ID
if database.AppInstallID > 0 {
installResource.LinkId = database.AppInstallID
} else {
installResource.LinkId = database.ID
}
installResource.Key = database.Type
installResource.From = database.From
if err := appInstallResourceRepo.Create(ctx, &installResource); err != nil {
return err
}
}
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
var installResource model.AppInstallResource
installResource.ResourceId = resourceId
installResource.AppInstallId = appInstall.ID
if database.AppInstallID > 0 {
installResource.LinkId = database.AppInstallID
} else {
installResource.LinkId = database.ID
}
installResource.Key = database.Type
installResource.From = database.From
return appInstallResourceRepo.Create(ctx, &installResource)
}
}
if err := deleteLink(ctx, install, true, true, true); err != nil {
return err
installTask.AddSubTask(task.GetTaskName(dbConfig.DbName, task.TaskCreate, task.TaskScopeDatabase), createAppDataBase, deleteAppLink)
}
return nil
}
@ -333,7 +322,8 @@ func deleteAppInstall(install model.AppInstall, deleteBackup bool, forceDelete b
if err != nil && !forceDelete {
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())
return err
}
@ -652,7 +642,8 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
return
}
if upErr = runScript(&install, "upgrade"); upErr != nil {
//TODO use task
if upErr = runScript(nil, &install, "upgrade"); upErr != nil {
return
}
@ -800,7 +791,7 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
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()
appResourceDir := path.Join(constant.AppResourceDir, app.Resource)
@ -853,7 +844,7 @@ func copyData(app model.App, appDetail model.AppDetail, appInstall *model.AppIns
return
}
func runScript(appInstall *model.AppInstall, operate string) error {
func runScript(task *task.Task, appInstall *model.AppInstall, operate string) error {
workDir := appInstall.GetPath()
scriptPath := ""
switch operate {
@ -867,15 +858,17 @@ func runScript(appInstall *model.AppInstall, operate string) error {
if !files.NewFileOp().Stat(scriptPath) {
return nil
}
logStr := i18n.GetWithName("ExecShell", operate)
task.LogStart(logStr)
out, err := cmd.ExecScript(scriptPath, workDir)
if err != nil {
if out != "" {
errMsg := fmt.Sprintf("run script %s error %s", scriptPath, out)
global.LOG.Error(errMsg)
return errors.New(errMsg)
err = errors.New(out)
}
task.LogFailedWithErr(logStr, err)
return err
}
task.LogSuccess(logStr)
return nil
}
@ -905,38 +898,59 @@ func checkContainerNameIsExist(containerName, appDir string) (bool, error) {
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) {
var (
out string
errMsg string
)
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 out != "" {
if strings.Contains(out, "no such host") {
errMsg = i18n.GetMsgByKey("ErrNoSuchHost") + ":"
}
if strings.Contains(out, "timeout") {
errMsg = i18n.GetMsgByKey("ErrImagePullTimeOut") + ":"
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 strings.Contains(out, "no such host") {
errMsg = i18n.GetMsgByKey("ErrNoSuchHost") + ":"
}
if strings.Contains(out, "timeout") {
errMsg = i18n.GetMsgByKey("ErrImagePullTimeOut") + ":"
}
}
appInstall.Message = errMsg + out
task.LogFailedWithErr(i18n.GetMsgByKey("PullImage"), err)
return err
} else {
task.Log(i18n.GetMsgByKey("PullImageSuccess"))
}
return err
}
}
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("Run"), i18n.GetMsgByKey("App"))
task.Log(logStr)
out, err = compose.Up(appInstall.GetComposePath())
if err != nil {
if out != "" {
appInstall.Message = errMsg + out
err = errors.New(out)
}
task.LogFailedWithErr(logStr, err)
return err
}
task.LogSuccess(logStr)
return
}
if err := upProject(appInstall); err != nil {
if appInstall.Message == "" {
appInstall.Message = err.Error()
}
appInstall.Status = constant.UpErr
} else {
appInstall.Status = constant.Running
@ -944,13 +958,12 @@ func upApp(appInstall *model.AppInstall, pullImages bool) {
exist, _ := appInstallRepo.GetFirst(commonRepo.WithByID(appInstall.ID))
if exist.ID > 0 {
containerNames, err := getContainerNames(*appInstall)
if err != nil {
return
}
if len(containerNames) > 0 {
appInstall.ContainerName = strings.Join(containerNames, ",")
if err == nil {
if len(containerNames) > 0 {
appInstall.ContainerName = strings.Join(containerNames, ",")
}
_ = appInstallRepo.Save(context.Background(), appInstall)
}
_ = appInstallRepo.Save(context.Background(), appInstall)
}
}

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

@ -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)
}
}
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))
}

@ -5,10 +5,12 @@ import (
"bytes"
"context"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/task"
"os"
"path"
"reflect"
@ -206,6 +208,13 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
if exist, _ := websiteRepo.GetBy(websiteRepo.WithAlias(alias)); len(exist) > 0 {
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)
if err != nil {
@ -249,23 +258,15 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
runtime *model.Runtime
)
defer func() {
if err != nil {
if website.AppInstallID > 0 {
req := request.AppInstalledOperate{
InstallId: website.AppInstallID,
Operate: constant.Delete,
ForceDelete: true,
}
if err := NewIAppInstalledService().Operate(req); err != nil {
global.LOG.Errorf(err.Error())
}
}
}
}()
createTask, err := task.NewTaskWithOps(create.PrimaryDomain, task.TaskCreate, task.TaskScopeWebsite, create.TaskID)
if err != nil {
return err
}
var proxy string
switch create.Type {
case constant.Deployment:
if create.AppType == constant.NewApp {
var (
@ -276,13 +277,10 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
req.AppDetailId = create.AppInstall.AppDetailId
req.Params = create.AppInstall.Params
req.AppContainerConfig = create.AppInstall.AppContainerConfig
tx, installCtx := getTxAndContext()
install, err = NewIAppService().Install(installCtx, req)
install, err = NewIAppService().Install(req)
if err != nil {
tx.Rollback()
return err
}
tx.Commit()
appInstall = install
website.AppInstallID = install.ID
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 {
return err
}
appInstall = &install
website.AppInstallID = appInstall.ID
website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort)
configApp := func(t *task.Task) error {
appInstall = &install
website.AppInstallID = appInstall.ID
website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort)
return nil
}
createTask.AddSubTask(i18n.GetMsgByKey("ConfigApp"), configApp, nil)
}
case constant.Runtime:
runtime, err = runtimeRepo.GetFirst(commonRepo.WithByID(create.RuntimeID))
@ -302,75 +304,89 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
return err
}
website.RuntimeID = runtime.ID
switch runtime.Type {
case constant.RuntimePHP:
if runtime.Resource == constant.ResourceAppstore {
var (
req request.AppInstallCreate
install *model.AppInstall
)
reg, _ := regexp.Compile(`[^a-z0-9_-]+`)
req.Name = reg.ReplaceAllString(strings.ToLower(alias), "")
req.AppDetailId = create.AppInstall.AppDetailId
req.Params = create.AppInstall.Params
req.Params["IMAGE_NAME"] = runtime.Image
req.AppContainerConfig = create.AppInstall.AppContainerConfig
req.Params["PANEL_WEBSITE_DIR"] = path.Join(nginxInstall.GetPath(), "/www")
tx, installCtx := getTxAndContext()
install, err = NewIAppService().Install(installCtx, req)
if err != nil {
tx.Rollback()
return err
}
tx.Commit()
website.AppInstallID = install.ID
appInstall = install
website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort)
} else {
website.ProxyType = create.ProxyType
if website.ProxyType == constant.RuntimeProxyUnix {
proxy = fmt.Sprintf("unix:%s", path.Join("/www/sites", website.Alias, "php-pool", "php-fpm.sock"))
}
if website.ProxyType == constant.RuntimeProxyTcp {
proxy = fmt.Sprintf("127.0.0.1:%d", create.Port)
}
website.Proxy = proxy
if runtime.Type == constant.RuntimePHP {
var (
req request.AppInstallCreate
install *model.AppInstall
)
reg, _ := regexp.Compile(`[^a-z0-9_-]+`)
req.Name = reg.ReplaceAllString(strings.ToLower(alias), "")
req.AppDetailId = create.AppInstall.AppDetailId
req.Params = create.AppInstall.Params
req.Params["IMAGE_NAME"] = runtime.Image
req.AppContainerConfig = create.AppInstall.AppContainerConfig
req.Params["PANEL_WEBSITE_DIR"] = path.Join(nginxInstall.GetPath(), "/www")
install, err = NewIAppService().Install(req)
if err != nil {
return err
}
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo:
website.Proxy = fmt.Sprintf("127.0.0.1:%d", runtime.Port)
website.AppInstallID = install.ID
appInstall = install
website.Proxy = fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort)
} else {
website.ProxyType = create.ProxyType
if website.ProxyType == constant.RuntimeProxyUnix {
proxy = fmt.Sprintf("unix:%s", path.Join("/www/sites", website.Alias, "php-pool", "php-fpm.sock"))
}
if website.ProxyType == constant.RuntimeProxyTcp {
proxy = fmt.Sprintf("127.0.0.1:%d", create.Port)
}
website.Proxy = proxy
}
}
if err = configDefaultNginx(website, domains, appInstall, runtime); err != nil {
return err
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo:
website.Proxy = fmt.Sprintf("127.0.0.1:%d", runtime.Port)
}
if len(create.FtpUser) != 0 && len(create.FtpPassword) != 0 {
indexDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "index")
itemID, err := NewIFtpService().Create(dto.FtpCreate{User: create.FtpUser, Password: create.FtpPassword, Path: indexDir})
if err != nil {
global.LOG.Errorf("create ftp for website failed, err: %v", err)
createFtpUser := func(t *task.Task) error {
indexDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "index")
itemID, err := NewIFtpService().Create(dto.FtpCreate{User: create.FtpUser, Password: create.FtpPassword, Path: indexDir})
if err != nil {
createTask.Log(fmt.Sprintf("create ftp for website failed, err: %v", err))
}
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())
}
}
}
website.FtpID = itemID
createTask.AddSubTask(i18n.GetWithName("ConfigFTP", create.FtpUser), createFtpUser, deleteFtpUser)
}
if err = createWafConfig(website, domains); err != nil {
return err
configNginx := func(t *task.Task) error {
if err = configDefaultNginx(website, domains, appInstall, runtime); err != nil {
return err
}
if err = createWafConfig(website, domains); err != nil {
return err
}
tx, ctx := helper.GetTxAndContext()
defer tx.Rollback()
if err = websiteRepo.Create(ctx, website); err != nil {
return err
}
for i := range domains {
domains[i].WebsiteID = website.ID
}
if err = websiteDomainRepo.BatchCreate(ctx, domains); err != nil {
return err
}
tx.Commit()
return nil
}
tx, ctx := helper.GetTxAndContext()
defer tx.Rollback()
if err = websiteRepo.Create(ctx, website); err != nil {
return err
}
for i := range domains {
domains[i].WebsiteID = website.ID
}
if err = websiteDomainRepo.BatchCreate(ctx, domains); err != nil {
return err
deleteWebsite := func() {
_ = deleteWebsiteFolder(nginxInstall, website)
}
tx.Commit()
return nil
createTask.AddSubTask(i18n.GetMsgByKey("ConfigOpenresty"), configNginx, deleteWebsite)
return createTask.Execute()
}
func (w WebsiteService) OpWebsite(req request.WebsiteOp) error {

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

@ -3,28 +3,35 @@ package task
import (
"context"
"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"
"os"
"path"
"strconv"
"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 Task struct {
Name string
TaskID string
Logger *log.Logger
SubTasks []*SubTask
Rollbacks []RollbackFunc
logFile *os.File
taskRepo repo.ITaskRepo
Task *model.Task
ParentID string
}
type SubTask struct {
RootTask *Task
Name string
Retry int
Timeout time.Duration
@ -33,51 +40,107 @@ type SubTask struct {
Error error
}
func NewTask(name string, taskType string) (*Task, error) {
logPath := path.Join(constant.LogDir, taskType)
//TODO 增加插入到日志表的逻辑
const (
TaskInstall = "TaskInstall"
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)
if err != nil {
return nil, fmt.Errorf("failed to open log file: %w", err)
}
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) {
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)
}
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)
}
func (s *SubTask) Execute(logger *log.Logger) bool {
logger.Printf(i18n.GetWithName("SubTaskStart", s.Name))
func (s *SubTask) Execute() error {
s.RootTask.Log(s.Name)
var err error
for i := 0; i < s.Retry+1; i++ {
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)
defer cancel()
done := make(chan error)
go func() {
done <- s.Action()
done <- s.Action(s.RootTask)
}()
select {
case <-ctx.Done():
logger.Printf(i18n.GetWithName("TaskTimeout", s.Name))
case err := <-done:
s.RootTask.Log(i18n.GetWithName("TaskTimeout", s.Name))
case err = <-done:
if err != nil {
s.Error = err
logger.Printf(i18n.GetWithNameAndErr("TaskFailed", s.Name, err))
s.RootTask.Log(i18n.GetWithNameAndErr("SubTaskFailed", s.Name, err))
} else {
logger.Printf(i18n.GetWithName("TaskSuccess", s.Name))
return true
s.RootTask.Log(i18n.GetWithName("SubTaskSuccess", s.Name))
return nil
}
}
@ -88,29 +151,77 @@ func (s *SubTask) Execute(logger *log.Logger) bool {
}
time.Sleep(1 * time.Second)
}
if s.Error != nil {
s.Error = fmt.Errorf(i18n.GetWithName("TaskFailed", s.Name))
}
return false
return err
}
func (t *Task) updateTask(task *model.Task) {
_ = t.taskRepo.Update(context.Background(), task)
}
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
t.Log(i18n.GetWithName("TaskStart", t.Name))
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 {
t.Rollbacks = append(t.Rollbacks, subTask.Rollback)
}
} else {
err = subTask.Error
t.Task.ErrorMsg = err.Error()
t.Task.Status = constant.StatusFailed
for _, rollback := range t.Rollbacks {
rollback()
}
t.updateTask(t.Task)
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()
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))
}

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

@ -7,10 +7,15 @@ const (
SystemRestart = "systemRestart"
TypeWebsite = "website"
TypePhp = "php"
TypeSSL = "ssl"
TypeSystem = "system"
TypeWebsite = "website"
TypePhp = "php"
TypeSSL = "ssl"
TypeSystem = "system"
TypeTask = "task"
TypeImagePull = "image-pull"
TypeImagePush = "image-push"
TypeImageBuild = "image-build"
TypeComposeCreate = "compose-create"
)
const (

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

@ -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"
#task
TaskStart: "{{.name}} started [START]"
TaskEnd: "{{.name}} ended [COMPLETED]"
TaskFailed: "{{.name}} failed: {{.err}}"
TaskTimeout: "{{.name}} timed out"
TaskSuccess: "{{.name}} succeeded"
TaskRetry: "Start {{.name}} retry"
SubTaskStart: "Start {{.name}}"
TaskStart: "{{.name}} Start [START]"
TaskEnd: "{{.name}} End [COMPLETED]"
TaskFailed: "{{.name}} Failed"
TaskTimeout: "{{.name}} Timeout"
TaskSuccess: "{{.name}} Success"
TaskRetry: "Starting {{.name}} Retry"
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"

@ -202,8 +202,33 @@ ErrXpackOutOfDate: "當前許可證已過期,請重新在 面板設置-許可
#task
TaskStart: "{{.name}} 開始 [START]"
TaskEnd: "{{.name}} 結束 [COMPLETED]"
TaskFailed: "{{.name}} 失敗: {{.err}}"
TaskTimeout: "{{.name}} 時"
TaskFailed: "{{.name}} 失敗"
TaskTimeout: "{{.name}} 時"
TaskSuccess: "{{.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: "啟動"

@ -203,8 +203,33 @@ ErrXpackOutOfDate: "当前许可证已过期,请重新在 面板设置-许可
#task
TaskStart: "{{.name}} 开始 [START]"
TaskEnd: "{{.name}} 结束 [COMPLETED]"
TaskFailed: "{{.name}} 失败: {{.err}}"
TaskFailed: "{{.name}} 失败"
TaskTimeout: "{{.name}} 超时"
TaskSuccess: "{{.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: "启动"

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

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

@ -6,7 +6,6 @@ import (
"os"
"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/init/app"
"github.com/1Panel-dev/1Panel/agent/init/business"
@ -40,25 +39,23 @@ func Start() {
server := &http.Server{
Handler: rootRouter,
}
if len(global.CurrentNode) == 0 || global.CurrentNode == "127.0.0.1" {
_ = os.Remove("/tmp/agent.sock")
listener, err := net.Listen("unix", "/tmp/agent.sock")
if err != nil {
panic(err)
}
_ = 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)
}
//ln, err := net.Listen("tcp4", "0.0.0.0:9998")
//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")
if err != nil {
panic(err)
}
server.Serve(listener)
}

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

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

@ -22,7 +22,7 @@ func Proxy() gin.HandlerFunc {
return
}
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"
if _, err := os.Stat(sockPath); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrProxy, err)

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

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

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

@ -19,7 +19,7 @@ export const CreateWebsite = (req: Website.WebSiteCreateReq) => {
if (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) => {

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

@ -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>

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

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

@ -29,7 +29,7 @@
</el-button>
<span v-if="errBuckets" class="input-error">{{ $t('commons.rule.requiredSelect') }}</span>
</el-form-item>
<el-form-item :label="$t('cronjob.requestExpirationTime')" prop="varsJson.timeout">
<el-form-item :label="$t('cronjob.requestExpirationTime')" prop="varsJson.timeout">
<el-input-number
style="width: 200px"
:min="1"

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

@ -13,10 +13,6 @@
<el-button type="primary" @click="openCreate">
{{ $t('runtime.create') }}
</el-button>
<el-button type="primary" plain @click="onOpenBuildCache()">
{{ $t('container.cleanBuildCache') }}
</el-button>
</template>
<template #main>
<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 AppResources from '@/views/website/runtime/php/check/index.vue';
import { ElMessageBox } from 'element-plus';
import { containerPrune } from '@/api/modules/container';
import { MsgSuccess } from '@/utils/message';
let timer: NodeJS.Timer | null = null;
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) => {
composeLogRef.value.acceptParams({ compose: row.path + '/docker-compose.yml', resource: row.name });
};

@ -13,10 +13,6 @@
<el-button type="primary" @click="openCreate">
{{ $t('runtime.create') }}
</el-button>
<el-button type="primary" plain @click="onOpenBuildCache()">
{{ $t('container.cleanBuildCache') }}
</el-button>
</template>
<template #main>
<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 AppResources from '@/views/website/runtime/php/check/index.vue';
import { ElMessageBox } from 'element-plus';
import { containerPrune } from '@/api/modules/container';
import { MsgSuccess } from '@/utils/message';
let timer: NodeJS.Timer | null = null;
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) => {
composeLogRef.value.acceptParams({ compose: row.path + '/docker-compose.yml', resource: row.name });
};

@ -13,10 +13,6 @@
<el-button type="primary" @click="openCreate">
{{ $t('runtime.create') }}
</el-button>
<el-button type="primary" plain @click="onOpenBuildCache()">
{{ $t('container.cleanBuildCache') }}
</el-button>
</template>
<template #main>
<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 AppResources from '@/views/website/runtime/php/check/index.vue';
import { ElMessageBox } from 'element-plus';
import { containerPrune } from '@/api/modules/container';
import { MsgSuccess } from '@/utils/message';
let timer: NodeJS.Timer | null = null;
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) => {
composeLogRef.value.acceptParams({ compose: row.path + '/docker-compose.yml', resource: row.name });
};

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

Loading…
Cancel
Save