1Panel/backend/app/service/app.go

785 lines
19 KiB
Go
Raw Normal View History

package service
import (
"encoding/base64"
"encoding/json"
2022-09-26 18:20:21 +08:00
"errors"
2022-10-07 15:49:39 +08:00
"fmt"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/app/repo"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
2022-09-23 16:33:55 +08:00
"github.com/1Panel-dev/1Panel/utils/common"
"github.com/1Panel-dev/1Panel/utils/compose"
"github.com/1Panel-dev/1Panel/utils/docker"
"github.com/1Panel-dev/1Panel/utils/files"
"golang.org/x/net/context"
"gopkg.in/yaml.v3"
"math"
"os"
"path"
"reflect"
"strconv"
"strings"
)
type AppService struct {
}
func (a AppService) Page(req dto.AppRequest) (interface{}, error) {
var opts []repo.DBOption
opts = append(opts, commonRepo.WithOrderBy("name"))
if req.Name != "" {
opts = append(opts, commonRepo.WithLikeName(req.Name))
}
2022-09-23 16:33:55 +08:00
if len(req.Tags) != 0 {
tags, err := tagRepo.GetByKeys(req.Tags)
if err != nil {
return nil, err
}
var tagIds []uint
for _, t := range tags {
tagIds = append(tagIds, t.ID)
}
appTags, err := appTagRepo.GetByTagIds(tagIds)
if err != nil {
return nil, err
}
var appIds []uint
for _, t := range appTags {
appIds = append(appIds, t.AppId)
}
opts = append(opts, commonRepo.WithIdsIn(appIds))
}
var res dto.AppRes
total, apps, err := appRepo.Page(req.Page, req.PageSize, opts...)
if err != nil {
return nil, err
}
var appDTOs []*dto.AppDTO
for _, a := range apps {
appDTO := &dto.AppDTO{
App: a,
}
appDTOs = append(appDTOs, appDTO)
appTags, err := appTagRepo.GetByAppId(a.ID)
if err != nil {
continue
}
var tagIds []uint
for _, at := range appTags {
tagIds = append(tagIds, at.TagId)
}
tags, err := tagRepo.GetByIds(tagIds)
if err != nil {
continue
}
appDTO.Tags = tags
}
res.Items = appDTOs
res.Total = total
tags, err := tagRepo.All()
if err != nil {
return nil, err
}
res.Tags = tags
return res, nil
}
2022-09-23 16:33:55 +08:00
func (a AppService) GetApp(id uint) (dto.AppDTO, error) {
var appDTO dto.AppDTO
app, err := appRepo.GetFirst(commonRepo.WithByID(id))
if err != nil {
return appDTO, err
}
appDTO.App = app
2022-10-03 17:35:39 +08:00
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID))
2022-09-23 16:33:55 +08:00
if err != nil {
return appDTO, err
}
var versionsRaw []string
for _, detail := range details {
versionsRaw = append(versionsRaw, detail.Version)
}
2022-10-03 17:35:39 +08:00
appDTO.Versions = common.GetSortedVersions(versionsRaw)
2022-09-23 16:33:55 +08:00
return appDTO, nil
}
func (a AppService) PageInstalled(req dto.AppInstalledRequest) (int64, []dto.AppInstalled, error) {
total, installed, err := appInstallRepo.Page(req.Page, req.PageSize)
if err != nil {
return 0, nil, err
}
installDTOs := []dto.AppInstalled{}
for _, in := range installed {
installDto := dto.AppInstalled{
AppInstall: in,
AppName: in.App.Name,
Icon: in.App.Icon,
}
installDTOs = append(installDTOs, installDto)
}
return total, installDTOs, nil
}
2022-09-23 16:33:55 +08:00
func (a AppService) GetAppDetail(appId uint, version string) (dto.AppDetailDTO, error) {
var (
appDetailDTO dto.AppDetailDTO
2022-09-30 17:56:06 +08:00
opts []repo.DBOption
2022-09-23 16:33:55 +08:00
)
opts = append(opts, appDetailRepo.WithAppId(appId), appDetailRepo.WithVersion(version))
2022-10-03 17:35:39 +08:00
detail, err := appDetailRepo.GetFirst(opts...)
2022-09-23 16:33:55 +08:00
if err != nil {
return appDetailDTO, err
}
paramMap := make(map[string]interface{})
json.Unmarshal([]byte(detail.Params), &paramMap)
2022-09-23 16:33:55 +08:00
appDetailDTO.AppDetail = detail
appDetailDTO.Params = paramMap
2022-09-23 16:33:55 +08:00
return appDetailDTO, nil
}
2022-09-26 18:20:21 +08:00
func (a AppService) Operate(req dto.AppInstallOperate) error {
2022-09-30 17:56:06 +08:00
install, err := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallId))
2022-09-26 18:20:21 +08:00
if err != nil {
return err
}
dockerComposePath := install.GetComposePath()
2022-09-26 18:20:21 +08:00
switch req.Operate {
case dto.Up:
out, err := compose.Up(dockerComposePath)
if err != nil {
return handleErr(install, err, out)
}
install.Status = constant.Running
2022-09-26 18:20:21 +08:00
case dto.Down:
out, err := compose.Down(dockerComposePath)
if err != nil {
return handleErr(install, err, out)
}
install.Status = constant.Stopped
2022-09-26 18:20:21 +08:00
case dto.Restart:
out, err := compose.Restart(dockerComposePath)
if err != nil {
return handleErr(install, err, out)
}
install.Status = constant.Running
2022-09-26 22:54:38 +08:00
case dto.Delete:
op := files.NewFileOp()
appDir := install.GetPath()
2022-09-26 22:54:38 +08:00
dir, _ := os.Stat(appDir)
tx := global.DB.Begin()
ctx := context.WithValue(context.Background(), "db", tx)
2022-09-26 22:54:38 +08:00
if dir == nil {
if err := appInstallRepo.Delete(ctx, install); err != nil {
return err
}
tx.Commit()
return nil
2022-09-26 22:54:38 +08:00
}
out, err := compose.Down(dockerComposePath)
if err != nil {
tx.Rollback()
return handleErr(install, err, out)
}
2022-09-30 17:56:06 +08:00
if err := op.DeleteDir(appDir); err != nil {
tx.Rollback()
return err
}
if err := appInstallRepo.Delete(ctx, install); err != nil {
tx.Rollback()
2022-09-30 17:56:06 +08:00
return err
2022-09-26 22:54:38 +08:00
}
database, _ := dataBaseRepo.GetFirst(dataBaseRepo.ByAppInstallId(install.ID))
if reflect.DeepEqual(database, model.Database{}) {
tx.Commit()
return nil
}
if err := dataBaseRepo.DeleteBy(ctx, dataBaseRepo.ByAppInstallId(install.ID)); err != nil {
tx.Rollback()
return err
}
container, err := appContainerRepo.GetFirst(commonRepo.WithByID(database.AppContainerId))
if err != nil {
tx.Commit()
return nil
}
if err := execDockerCommand(database, container, Delete); err != nil {
tx.Rollback()
return err
}
tx.Commit()
return nil
case dto.Sync:
if err := a.SyncInstalled(install.ID); err != nil {
return err
}
return nil
2022-09-26 18:20:21 +08:00
default:
return errors.New("operate not support")
}
return appInstallRepo.Save(install)
2022-09-26 18:20:21 +08:00
}
func handleErr(install model.AppInstall, err error, out string) error {
reErr := err
install.Message = err.Error()
if out != "" {
install.Message = out
reErr = errors.New(out)
}
_ = appInstallRepo.Save(install)
return reErr
}
func (a AppService) Install(name string, appDetailId uint, params map[string]interface{}) error {
port, ok := params["PANEL_APP_PORT"]
if ok {
2022-09-27 22:53:24 +08:00
portStr := strconv.FormatFloat(port.(float64), 'f', -1, 32)
if common.ScanPort(portStr) {
2022-10-07 15:49:39 +08:00
return errors.New("port is in used")
}
}
2022-10-03 17:35:39 +08:00
appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(appDetailId))
if err != nil {
return err
}
app, err := appRepo.GetFirst(commonRepo.WithByID(appDetail.AppId))
2022-10-07 15:49:39 +08:00
if err != nil {
return err
}
2022-10-07 15:49:39 +08:00
if app.Required != "" {
var requiredArray []string
if err := json.Unmarshal([]byte(app.Required), &requiredArray); err != nil {
return err
}
for _, key := range requiredArray {
if key == "" {
continue
}
requireApp, err := appRepo.GetFirst(appRepo.WithKey(key))
if err != nil {
return err
}
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(requireApp.ID))
if err != nil {
return err
}
var detailIds []uint
for _, d := range details {
detailIds = append(detailIds, d.ID)
}
_, err = appInstallRepo.GetFirst(appInstallRepo.WithDetailIdsIn(detailIds))
if err != nil {
return errors.New(fmt.Sprintf("%s is required", requireApp.Key))
}
}
}
paramByte, err := json.Marshal(params)
if err != nil {
return err
}
appInstall := model.AppInstall{
Name: name,
AppId: appDetail.AppId,
AppDetailId: appDetail.ID,
Version: appDetail.Version,
Status: constant.Installing,
Params: string(paramByte),
}
composeFilePath, err := copyAppData(app.Key, appDetail.Version, name, params)
if err != nil {
return err
}
var (
auth string
dbConfig dto.AppDatabase
)
tags, err := tagRepo.GetByAppId(app.ID)
if err != nil {
return err
}
for _, tag := range tags {
if tag.Key == "Database" {
var authParam dto.AuthParam
paramByte, err := json.Marshal(params)
if err != nil {
return err
}
if err := json.Unmarshal(paramByte, &authParam); err != nil {
return err
}
authByte, err := json.Marshal(authParam)
if err != nil {
return err
}
auth = string(authByte)
}
if tag.Key == "WebSite" {
paramByte, err := json.Marshal(params)
if err != nil {
return err
}
if err := json.Unmarshal(paramByte, &dbConfig); err != nil {
return err
}
}
}
composeMap := make(map[string]interface{})
2022-10-07 15:49:39 +08:00
if err := yaml.Unmarshal([]byte(appDetail.DockerCompose), &composeMap); err != nil {
return err
}
servicesMap := composeMap["services"].(map[string]interface{})
changeKeys := make(map[string]string, len(servicesMap))
var appContainers []*model.AppContainer
for k, v := range servicesMap {
serviceName := k + "-" + common.RandStr(4)
changeKeys[k] = serviceName
value := v.(map[string]interface{})
containerName := constant.ContainerPrefix + k + "-" + common.RandStr(4)
value["container_name"] = containerName
2022-10-07 15:49:39 +08:00
servicePort := 0
port, ok := params["PANEL_APP_PORT"]
if ok {
portN := int(math.Ceil(port.(float64)))
servicePort = portN
2022-10-07 15:49:39 +08:00
}
appContainers = append(appContainers, &model.AppContainer{
ServiceName: serviceName,
ContainerName: containerName,
2022-10-07 15:49:39 +08:00
Port: servicePort,
Auth: auth,
})
}
for k, v := range changeKeys {
servicesMap[v] = servicesMap[k]
delete(servicesMap, k)
}
2022-10-07 15:49:39 +08:00
composeByte, err := yaml.Marshal(composeMap)
if err != nil {
return err
}
fileOp := files.NewFileOp()
2022-10-07 15:49:39 +08:00
if err := fileOp.WriteFile(composeFilePath, strings.NewReader(string(composeByte)), 0775); err != nil {
return err
}
tx := global.DB.Begin()
ctx := context.WithValue(context.Background(), "db", tx)
if err := appInstallRepo.Create(ctx, &appInstall); err != nil {
tx.Rollback()
return err
}
for _, c := range appContainers {
c.AppInstallID = appInstall.ID
}
if err := appContainerRepo.BatchCreate(ctx, appContainers); err != nil {
tx.Rollback()
return err
}
if !reflect.DeepEqual(dbConfig, dto.AppDatabase{}) {
container, err := appContainerRepo.GetFirst(appContainerRepo.WithServiceName(dbConfig.ServiceName))
if err != nil {
tx.Rollback()
return err
}
install, err := appInstallRepo.GetFirst(commonRepo.WithByID(container.AppInstallID))
if err != nil {
tx.Rollback()
return err
}
app, err := appRepo.GetFirst(commonRepo.WithByID(install.ID))
if err != nil {
tx.Rollback()
return err
}
var database model.Database
database.AppContainerId = container.ID
database.Dbname = dbConfig.DbName
database.Username = dbConfig.DbUser
database.Password = dbConfig.Password
database.AppInstallId = appInstall.ID
database.Key = app.Key
if err := dataBaseRepo.Create(ctx, &database); err != nil {
tx.Rollback()
return err
}
if err := execDockerCommand(database, container, Add); err != nil {
tx.Rollback()
return err
}
}
tx.Commit()
go upApp(composeFilePath, appInstall)
return nil
}
func (a AppService) SyncAllInstalled() error {
allList, err := appInstallRepo.GetBy()
if err != nil {
return err
}
go func() {
for _, i := range allList {
if err := a.SyncInstalled(i.ID); err != nil {
global.LOG.Errorf("sync install app[%s] error,mgs: %s", i.Name, err.Error())
}
}
}()
return nil
}
2022-10-07 15:49:39 +08:00
func (a AppService) GetServices(key string) ([]dto.AppService, error) {
app, err := appRepo.GetFirst(appRepo.WithKey(key))
if err != nil {
return nil, err
}
installs, err := appInstallRepo.GetBy(appInstallRepo.WithAppId(app.ID), appInstallRepo.WithStatus(constant.Running))
if err != nil {
return nil, err
}
var res []dto.AppService
for _, install := range installs {
for _, container := range install.Containers {
res = append(res, dto.AppService{
Label: install.Name,
Value: container.ServiceName,
2022-10-07 15:49:39 +08:00
})
}
}
return res, nil
}
func (a AppService) SyncInstalled(installId uint) error {
appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(installId))
if err != nil {
return err
}
var containerNames []string
for _, a := range appInstall.Containers {
containerNames = append(containerNames, a.ContainerName)
}
cli, err := docker.NewClient()
if err != nil {
return err
}
containers, err := cli.ListContainersByName(containerNames)
if err != nil {
return err
}
2022-09-30 17:56:06 +08:00
var (
errorContainers []string
notFoundContainers []string
runningContainers []string
)
for _, n := range containers {
if n.State != "running" {
2022-09-30 17:56:06 +08:00
errorContainers = append(errorContainers, n.Names[0])
} else {
runningContainers = append(runningContainers, n.Names[0])
}
}
for _, old := range containerNames {
exist := false
for _, new := range containers {
if common.ExistWithStrArray(old, new.Names) {
exist = true
break
}
}
if !exist {
notFoundContainers = append(notFoundContainers, old)
}
}
2022-09-30 17:56:06 +08:00
containerCount := len(containers)
errCount := len(errorContainers)
notFoundCount := len(notFoundContainers)
normalCount := len(containerNames)
runningCount := len(runningContainers)
if containerCount == 0 {
appInstall.Status = constant.Error
appInstall.Message = "container is not found"
return appInstallRepo.Save(appInstall)
}
2022-09-30 17:56:06 +08:00
if errCount == 0 && notFoundCount == 0 {
appInstall.Status = constant.Running
return appInstallRepo.Save(appInstall)
}
2022-09-30 17:56:06 +08:00
if errCount == normalCount {
appInstall.Status = constant.Error
}
2022-09-30 17:56:06 +08:00
if notFoundCount == normalCount {
appInstall.Status = constant.Stopped
}
2022-09-30 17:56:06 +08:00
if runningCount < normalCount {
appInstall.Status = constant.UnHealthy
}
var errMsg strings.Builder
2022-09-30 17:56:06 +08:00
if errCount > 0 {
errMsg.Write([]byte(string(rune(errCount)) + " error containers:"))
for _, e := range errorContainers {
errMsg.Write([]byte(e))
}
errMsg.Write([]byte("\n"))
}
2022-09-30 17:56:06 +08:00
if notFoundCount > 0 {
errMsg.Write([]byte(string(rune(notFoundCount)) + " not found containers:"))
for _, e := range notFoundContainers {
errMsg.Write([]byte(e))
}
errMsg.Write([]byte("\n"))
}
appInstall.Message = errMsg.String()
return appInstallRepo.Save(appInstall)
}
func (a AppService) SyncAppList() error {
//TODO 从 oss 拉取最新列表
appDir := path.Join(global.CONF.System.ResourceDir, "apps")
iconDir := path.Join(appDir, "icons")
listFile := path.Join(appDir, "list.json")
content, err := os.ReadFile(listFile)
if err != nil {
return err
}
list := &dto.AppList{}
if err := json.Unmarshal(content, list); err != nil {
return err
}
var (
2022-09-30 17:56:06 +08:00
tags []*model.Tag
appTags []*model.AppTag
)
for _, t := range list.Tags {
tags = append(tags, &model.Tag{
Key: t.Key,
Name: t.Name,
})
}
2022-09-30 17:56:06 +08:00
oldApps, err := appRepo.GetBy()
if err != nil {
return err
}
appsMap := getApps(oldApps, list.Items)
for _, l := range list.Items {
2022-09-30 17:56:06 +08:00
app := appsMap[l.Key]
icon, err := os.ReadFile(path.Join(iconDir, l.Icon))
if err != nil {
global.LOG.Errorf("get [%s] icon error: %s", l.Name, err.Error())
continue
}
iconStr := base64.StdEncoding.EncodeToString(icon)
2022-09-30 17:56:06 +08:00
app.Icon = iconStr
app.TagsKey = l.Tags
versions := l.Versions
2022-09-30 17:56:06 +08:00
detailsMap := getAppDetails(app.Details, versions)
for _, v := range versions {
2022-09-30 17:56:06 +08:00
detail := detailsMap[v]
detailPath := path.Join(appDir, l.Key, v)
if _, err := os.Stat(detailPath); err != nil {
global.LOG.Errorf("get [%s] folder error: %s", detailPath, err.Error())
continue
}
readmeStr, err := os.ReadFile(path.Join(detailPath, "README.md"))
if err != nil {
global.LOG.Errorf("get [%s] README error: %s", detailPath, err.Error())
}
detail.Readme = string(readmeStr)
dockerComposeStr, err := os.ReadFile(path.Join(detailPath, "docker-compose.yml"))
if err != nil {
global.LOG.Errorf("get [%s] docker-compose.yml error: %s", detailPath, err.Error())
continue
}
detail.DockerCompose = string(dockerComposeStr)
paramStr, err := os.ReadFile(path.Join(detailPath, "params.json"))
if err != nil {
global.LOG.Errorf("get [%s] form.json error: %s", detailPath, err.Error())
}
detail.Params = string(paramStr)
2022-09-30 17:56:06 +08:00
detailsMap[v] = detail
}
var newDetails []model.AppDetail
for _, v := range detailsMap {
newDetails = append(newDetails, v)
}
2022-09-30 17:56:06 +08:00
app.Details = newDetails
appsMap[l.Key] = app
}
2022-09-30 17:56:06 +08:00
var (
addAppArray []model.App
updateArray []model.App
)
tagMap := make(map[string]uint, len(tags))
for _, v := range appsMap {
if v.ID == 0 {
addAppArray = append(addAppArray, v)
} else {
updateArray = append(updateArray, v)
}
}
tx := global.DB.Begin()
ctx := context.WithValue(context.Background(), "db", tx)
2022-09-30 17:56:06 +08:00
if len(addAppArray) > 0 {
if err := appRepo.BatchCreate(ctx, addAppArray); err != nil {
tx.Rollback()
return err
}
}
if err := tagRepo.DeleteAll(ctx); err != nil {
tx.Rollback()
return err
}
if len(tags) > 0 {
if err := tagRepo.BatchCreate(ctx, tags); err != nil {
tx.Rollback()
return err
}
2022-09-30 17:56:06 +08:00
for _, t := range tags {
tagMap[t.Key] = t.ID
}
}
2022-09-30 17:56:06 +08:00
for _, update := range updateArray {
if err := appRepo.Save(ctx, &update); err != nil {
tx.Rollback()
return err
}
}
2022-09-30 17:56:06 +08:00
apps := append(addAppArray, updateArray...)
var (
2022-09-30 17:56:06 +08:00
addDetails []model.AppDetail
updateDetails []model.AppDetail
)
for _, a := range apps {
for _, t := range a.TagsKey {
tagId, ok := tagMap[t]
if ok {
appTags = append(appTags, &model.AppTag{
AppId: a.ID,
TagId: tagId,
})
}
}
for _, d := range a.Details {
d.AppId = a.ID
2022-09-30 17:56:06 +08:00
if d.ID == 0 {
addDetails = append(addDetails, d)
} else {
updateDetails = append(updateDetails, d)
}
}
}
2022-09-30 17:56:06 +08:00
if len(addDetails) > 0 {
if err := appDetailRepo.BatchCreate(ctx, addDetails); err != nil {
tx.Rollback()
return err
}
}
2022-09-30 17:56:06 +08:00
for _, u := range updateDetails {
if err := appDetailRepo.Update(ctx, u); err != nil {
tx.Rollback()
return err
}
}
2022-09-30 17:56:06 +08:00
if err := appTagRepo.DeleteAll(ctx); err != nil {
tx.Rollback()
return err
}
if len(appTags) > 0 {
if err := appTagRepo.BatchCreate(ctx, appTags); err != nil {
tx.Rollback()
return err
}
}
tx.Commit()
2022-10-03 17:35:39 +08:00
go syncCanUpdate()
return nil
}
2022-10-03 17:35:39 +08:00
func syncCanUpdate() {
apps, err := appRepo.GetBy()
if err != nil {
global.LOG.Errorf("sync update app error: %s", err.Error())
}
for _, app := range apps {
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID))
if err != nil {
global.LOG.Errorf("sync update app error: %s", err.Error())
}
var versions []string
for _, detail := range details {
versions = append(versions, detail.Version)
}
versions = common.GetSortedVersions(versions)
lastVersion := versions[0]
var updateDetailIds []uint
for _, detail := range details {
if common.CompareVersion(lastVersion, detail.Version) {
if app.CrossVersionUpdate || !common.IsCrossVersion(detail.Version, lastVersion) {
updateDetailIds = append(updateDetailIds, detail.ID)
}
}
}
if len(updateDetailIds) > 0 {
if err := appDetailRepo.BatchUpdateBy(model.AppDetail{LastVersion: lastVersion}, commonRepo.WithIdsIn(updateDetailIds)); err != nil {
global.LOG.Errorf("sync update app error: %s", err.Error())
}
if err := appInstallRepo.BatchUpdateBy(model.AppInstall{CanUpdate: true}, appInstallRepo.WithDetailIdsIn(updateDetailIds)); err != nil {
global.LOG.Errorf("sync update app error: %s", err.Error())
}
}
}
}