2022-10-10 15:10:53 +08:00
package service
import (
2022-10-11 16:27:58 +08:00
"context"
2022-10-10 15:10:53 +08:00
"encoding/json"
"fmt"
2022-12-01 15:48:40 +08:00
"github.com/1Panel-dev/1Panel/backend/buserr"
2022-10-18 18:39:45 +08:00
"io/ioutil"
"math"
"net/http"
"os"
"path"
"reflect"
"strconv"
"strings"
"time"
2022-10-17 16:32:31 +08:00
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/compose"
"github.com/1Panel-dev/1Panel/backend/utils/files"
2022-10-10 15:10:53 +08:00
"github.com/joho/godotenv"
2022-10-10 22:56:42 +08:00
"github.com/pkg/errors"
2022-10-13 16:46:38 +08:00
"gopkg.in/yaml.v3"
2022-10-10 15:10:53 +08:00
)
type DatabaseOp string
var (
Add DatabaseOp = "add"
Delete DatabaseOp = "delete"
)
2022-11-18 17:50:52 +08:00
func execDockerCommand ( database model . DatabaseMysql , dbInstall model . AppInstall , op DatabaseOp ) error {
2022-10-10 15:10:53 +08:00
var auth dto . AuthParam
var dbConfig dto . AppDatabase
dbConfig . Password = database . Password
dbConfig . DbUser = database . Username
2022-11-18 17:50:52 +08:00
dbConfig . DbName = database . Name
2022-10-20 16:15:11 +08:00
_ = json . Unmarshal ( [ ] byte ( dbInstall . Param ) , & auth )
2022-10-10 15:10:53 +08:00
execConfig := dto . ContainerExec {
2022-10-11 16:27:58 +08:00
ContainerName : dbInstall . ContainerName ,
2022-10-10 15:10:53 +08:00
Auth : auth ,
DbParam : dbConfig ,
}
2022-11-18 17:50:52 +08:00
out , err := cmd . Exec ( getSqlStr ( dbInstall . Version , op , execConfig ) )
2022-10-10 15:10:53 +08:00
if err != nil {
2022-11-18 17:00:15 +08:00
return errors . New ( out )
2022-10-10 15:10:53 +08:00
}
return nil
}
2022-11-18 17:00:15 +08:00
func getSqlStr ( version string , operate DatabaseOp , exec dto . ContainerExec ) string {
2022-10-10 15:10:53 +08:00
var str string
param := exec . DbParam
2022-11-18 17:00:15 +08:00
if strings . Contains ( version , "5.7" ) {
2022-10-10 15:10:53 +08:00
if operate == Add {
2022-11-18 17:50:52 +08:00
str = fmt . Sprintf ( "docker exec -i %s mysql -uroot -p%s -e \"CREATE USER IF NOT EXISTS '%s'@'%%' IDENTIFIED BY '%s';\" -e \"create database %s;\" -e \"GRANT ALL ON %s.* TO '%s'@'%%' IDENTIFIED BY '%s';\" -e \"FLUSH PRIVILEGES;\"" ,
2022-10-24 17:46:08 +08:00
exec . ContainerName , exec . Auth . RootPassword , param . DbUser , param . Password , param . DbName , param . DbName , param . DbUser , param . Password )
}
if operate == Delete {
str = fmt . Sprintf ( "docker exec -i %s mysql -uroot -p%s -e \"drop database %s;\" -e \"drop user %s;\" " ,
exec . ContainerName , exec . Auth . RootPassword , param . DbName , param . DbUser )
}
2022-11-18 17:00:15 +08:00
}
if strings . Contains ( version , "8.0" ) {
2022-10-24 17:46:08 +08:00
if operate == Add {
2022-11-18 17:50:52 +08:00
str = fmt . Sprintf ( "docker exec -i %s mysql -uroot -p%s -e \"CREATE USER IF NOT EXISTS '%s'@'%%' IDENTIFIED BY '%s';\" -e \"create database %s;\" -e \"GRANT ALL ON %s.* TO '%s'@'%%';\" -e \"FLUSH PRIVILEGES;\"" ,
2022-10-24 17:46:08 +08:00
exec . ContainerName , exec . Auth . RootPassword , param . DbUser , param . Password , param . DbName , param . DbName , param . DbUser )
2022-10-10 15:10:53 +08:00
}
if operate == Delete {
str = fmt . Sprintf ( "docker exec -i %s mysql -uroot -p%s -e \"drop database %s;\" -e \"drop user %s;\" " ,
exec . ContainerName , exec . Auth . RootPassword , param . DbName , param . DbUser )
}
}
2022-11-18 17:00:15 +08:00
2022-10-10 15:10:53 +08:00
return str
}
2022-10-11 16:27:58 +08:00
func checkPort ( key string , params map [ string ] interface { } ) ( int , error ) {
port , ok := params [ key ]
if ok {
portN := int ( math . Ceil ( port . ( float64 ) ) )
if common . ScanPort ( portN ) {
2022-12-01 15:48:40 +08:00
return portN , buserr . New ( constant . ErrPortInUsed , portN , nil )
2022-10-11 16:27:58 +08:00
} else {
return portN , nil
}
}
return 0 , nil
}
func createLink ( ctx context . Context , app model . App , appInstall * model . AppInstall , params map [ string ] interface { } ) error {
var dbConfig dto . AppDatabase
if app . Type == "runtime" {
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
}
appInstall . Param = string ( authByte )
}
if app . Type == "website" {
paramByte , err := json . Marshal ( params )
if err != nil {
return err
}
if err := json . Unmarshal ( paramByte , & dbConfig ) ; err != nil {
return err
}
}
if ! reflect . DeepEqual ( dbConfig , dto . AppDatabase { } ) {
dbInstall , err := appInstallRepo . GetFirst ( appInstallRepo . WithServiceName ( dbConfig . ServiceName ) )
if err != nil {
return err
}
2022-11-18 17:50:52 +08:00
var database model . DatabaseMysql
database . Name = dbConfig . DbName
2022-10-11 16:27:58 +08:00
database . Username = dbConfig . DbUser
database . Password = dbConfig . Password
2022-11-18 17:50:52 +08:00
database . MysqlName = dbInstall . Name
database . Format = "utf8mb4"
database . Permission = "127.0.0.1"
if err := mysqlRepo . Create ( ctx , & database ) ; err != nil {
2022-10-11 16:27:58 +08:00
return err
}
var installResource model . AppInstallResource
installResource . ResourceId = database . ID
installResource . AppInstallId = appInstall . ID
installResource . LinkId = dbInstall . ID
installResource . Key = dbInstall . App . Key
if err := appInstallResourceRepo . Create ( ctx , & installResource ) ; err != nil {
return err
}
if err := execDockerCommand ( database , dbInstall , Add ) ; err != nil {
return err
}
}
return nil
}
2022-11-02 15:19:14 +08:00
func deleteAppInstall ( ctx context . Context , install model . AppInstall ) error {
2022-10-17 16:32:31 +08:00
op := files . NewFileOp ( )
appDir := install . GetPath ( )
dir , _ := os . Stat ( appDir )
if dir != nil {
out , err := compose . Down ( install . GetComposePath ( ) )
if err != nil {
return handleErr ( install , err , out )
}
if err := op . DeleteDir ( appDir ) ; err != nil {
return err
}
}
if err := appInstallRepo . Delete ( ctx , install ) ; err != nil {
return err
}
if err := deleteLink ( ctx , & install ) ; err != nil {
return err
}
if err := appInstallBackupRepo . Delete ( ctx , appInstallBackupRepo . WithAppInstallID ( install . ID ) ) ; err != nil {
return err
}
return nil
}
2022-10-11 16:27:58 +08:00
func deleteLink ( ctx context . Context , install * model . AppInstall ) error {
resources , _ := appInstallResourceRepo . GetBy ( appInstallResourceRepo . WithAppInstallId ( install . ID ) )
if len ( resources ) == 0 {
return nil
}
for _ , re := range resources {
if re . Key == "mysql" {
2022-11-18 17:50:52 +08:00
database , _ := mysqlRepo . Get ( commonRepo . WithByID ( re . ResourceId ) )
if reflect . DeepEqual ( database , model . DatabaseMysql { } ) {
2022-10-11 16:27:58 +08:00
continue
}
2022-11-18 17:50:52 +08:00
appInstall , err := appInstallRepo . GetFirst ( commonRepo . WithByName ( database . MysqlName ) )
2022-10-11 16:27:58 +08:00
if err != nil {
2022-11-18 17:50:52 +08:00
return err
2022-10-11 16:27:58 +08:00
}
if err := execDockerCommand ( database , appInstall , Delete ) ; err != nil {
return err
}
2022-11-18 17:50:52 +08:00
if err := mysqlRepo . Delete ( ctx , commonRepo . WithByID ( database . ID ) ) ; err != nil {
2022-10-11 16:27:58 +08:00
return err
}
}
}
return appInstallResourceRepo . DeleteBy ( ctx , appInstallResourceRepo . WithAppInstallId ( install . ID ) )
}
2022-10-13 18:56:53 +08:00
func updateInstall ( installId uint , detailId uint ) error {
install , err := appInstallRepo . GetFirst ( commonRepo . WithByID ( installId ) )
if err != nil {
return err
}
detail , err := appDetailRepo . GetFirst ( commonRepo . WithByID ( detailId ) )
if err != nil {
return err
}
if install . Version == detail . Version {
2022-10-17 16:32:31 +08:00
return errors . New ( "two version is same" )
2022-10-13 18:56:53 +08:00
}
tx , ctx := getTxAndContext ( )
if err := backupInstall ( ctx , install ) ; err != nil {
return err
}
tx . Commit ( )
if _ , err = compose . Down ( install . GetComposePath ( ) ) ; err != nil {
return err
}
install . DockerCompose = detail . DockerCompose
install . Version = detail . Version
2022-10-17 16:32:31 +08:00
2022-10-13 18:56:53 +08:00
fileOp := files . NewFileOp ( )
if err := fileOp . WriteFile ( install . GetComposePath ( ) , strings . NewReader ( install . DockerCompose ) , 0775 ) ; err != nil {
return err
}
if _ , err = compose . Up ( install . GetComposePath ( ) ) ; err != nil {
return err
}
return appInstallRepo . Save ( & install )
}
2022-10-12 18:57:22 +08:00
func backupInstall ( ctx context . Context , install model . AppInstall ) error {
var backup model . AppInstallBackup
appPath := install . GetPath ( )
backupDir := path . Join ( constant . BackupDir , install . App . Key , install . Name )
fileOp := files . NewFileOp ( )
now := time . Now ( )
day := now . Format ( "2006-01-02-15-04" )
fileName := fmt . Sprintf ( "%s-%s-%s%s" , install . Name , install . Version , day , ".tar.gz" )
if err := fileOp . Compress ( [ ] string { appPath } , backupDir , fileName , files . TarGz ) ; err != nil {
return err
}
backup . Name = fileName
backup . Path = backupDir
backup . AppInstallId = install . ID
backup . AppDetailId = install . AppDetailId
2022-10-13 16:46:38 +08:00
backup . Param = install . Param
2022-10-12 18:57:22 +08:00
2022-11-21 11:27:56 +08:00
return appInstallBackupRepo . Create ( ctx , backup )
2022-10-12 18:57:22 +08:00
}
2022-10-17 16:32:31 +08:00
func restoreInstall ( install model . AppInstall , backupId uint ) error {
backup , err := appInstallBackupRepo . GetFirst ( commonRepo . WithByID ( backupId ) )
if err != nil {
return err
}
2022-10-13 16:46:38 +08:00
if _ , err := compose . Down ( install . GetComposePath ( ) ) ; err != nil {
return err
}
installKeyDir := path . Join ( constant . AppInstallDir , install . App . Key )
installDir := path . Join ( installKeyDir , install . Name )
backupFile := path . Join ( backup . Path , backup . Name )
fileOp := files . NewFileOp ( )
if ! fileOp . Stat ( backupFile ) {
return errors . New ( fmt . Sprintf ( "%s file is not exist" , backup . Name ) )
}
2022-10-14 14:48:55 +08:00
backupDir , err := fileOp . Backup ( installDir )
if err != nil {
2022-10-13 16:46:38 +08:00
return err
}
if err := fileOp . Decompress ( backupFile , installKeyDir , files . TarGz ) ; err != nil {
return err
}
composeContent , err := os . ReadFile ( install . GetComposePath ( ) )
if err != nil {
return err
}
install . DockerCompose = string ( composeContent )
envContent , err := os . ReadFile ( path . Join ( installDir , ".env" ) )
if err != nil {
return err
}
install . Env = string ( envContent )
envMaps , err := godotenv . Unmarshal ( string ( envContent ) )
if err != nil {
return err
}
install . HttpPort = 0
httpPort , ok := envMaps [ "PANEL_APP_PORT_HTTP" ]
if ok {
httpPortN , _ := strconv . Atoi ( httpPort )
install . HttpPort = httpPortN
}
install . HttpsPort = 0
httpsPort , ok := envMaps [ "PANEL_APP_PORT_HTTPS" ]
if ok {
httpsPortN , _ := strconv . Atoi ( httpsPort )
install . HttpsPort = httpsPortN
}
composeMap := make ( map [ string ] interface { } )
if err := yaml . Unmarshal ( composeContent , & composeMap ) ; err != nil {
return err
}
servicesMap := composeMap [ "services" ] . ( map [ string ] interface { } )
for k , v := range servicesMap {
install . ServiceName = k
value := v . ( map [ string ] interface { } )
install . ContainerName = value [ "container_name" ] . ( string )
}
install . Param = backup . Param
2022-10-14 14:48:55 +08:00
_ = fileOp . DeleteDir ( backupDir )
2022-10-13 16:46:38 +08:00
if out , err := compose . Up ( install . GetComposePath ( ) ) ; err != nil {
return handleErr ( install , err , out )
}
2022-10-13 18:56:53 +08:00
install . AppDetailId = backup . AppDetailId
2022-10-17 16:32:31 +08:00
install . Version = backup . AppDetail . Version
2022-10-13 16:46:38 +08:00
install . Status = constant . Running
return appInstallRepo . Save ( & install )
}
2022-10-11 16:27:58 +08:00
func getContainerNames ( install model . AppInstall ) ( [ ] string , error ) {
composeMap := install . DockerCompose
envMap := make ( map [ string ] string )
_ = json . Unmarshal ( [ ] byte ( install . Env ) , & envMap )
project , err := compose . GetComposeProject ( [ ] byte ( composeMap ) , envMap )
if err != nil {
return nil , err
}
var containerNames [ ] string
for _ , service := range project . AllServices ( ) {
containerNames = append ( containerNames , service . ContainerName )
}
return containerNames , nil
}
2022-10-10 22:56:42 +08:00
func checkRequiredAndLimit ( app model . App ) error {
if app . Limit > 0 {
installs , err := appInstallRepo . GetBy ( appInstallRepo . WithAppId ( app . ID ) )
if err != nil {
return err
}
if len ( installs ) >= app . Limit {
return errors . New ( fmt . Sprintf ( "app install limit %d" , app . Limit ) )
}
}
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 ) )
}
}
}
return nil
}
2022-10-13 16:46:38 +08:00
func handleMap ( params map [ string ] interface { } , envParams map [ string ] string ) {
for k , v := range params {
switch t := v . ( type ) {
case string :
envParams [ k ] = t
case float64 :
envParams [ k ] = strconv . FormatFloat ( t , 'f' , - 1 , 32 )
default :
envParams [ k ] = t . ( string )
}
}
}
2022-10-11 16:27:58 +08:00
func copyAppData ( key , version , installName string , params map [ string ] interface { } ) ( err error ) {
2022-11-18 14:27:40 +08:00
resourceDir := path . Join ( constant . AppResourceDir , key , "versions" , version )
2022-10-12 18:57:22 +08:00
installDir := path . Join ( constant . AppInstallDir , key )
2022-10-10 15:10:53 +08:00
installVersionDir := path . Join ( installDir , version )
fileOp := files . NewFileOp ( )
2022-11-20 22:49:32 +08:00
if fileOp . Stat ( installVersionDir ) {
if err = fileOp . DeleteDir ( installVersionDir ) ; err != nil {
return
}
}
2022-10-10 15:10:53 +08:00
if err = fileOp . Copy ( resourceDir , installVersionDir ) ; err != nil {
return
}
appDir := path . Join ( installDir , installName )
if err = fileOp . Rename ( installVersionDir , appDir ) ; err != nil {
return
}
envPath := path . Join ( appDir , ".env" )
envParams := make ( map [ string ] string , len ( params ) )
2022-10-13 16:46:38 +08:00
handleMap ( params , envParams )
2022-10-10 15:10:53 +08:00
if err = godotenv . Write ( envParams , envPath ) ; err != nil {
return
}
return
}
func upApp ( composeFilePath string , appInstall model . AppInstall ) {
out , err := compose . Up ( composeFilePath )
if err != nil {
if out != "" {
appInstall . Message = out
} else {
appInstall . Message = err . Error ( )
}
appInstall . Status = constant . Error
2022-10-13 16:46:38 +08:00
_ = appInstallRepo . Save ( & appInstall )
2022-10-10 15:10:53 +08:00
} else {
appInstall . Status = constant . Running
2022-10-13 16:46:38 +08:00
_ = appInstallRepo . Save ( & appInstall )
2022-10-10 15:10:53 +08:00
}
}
func getAppDetails ( details [ ] model . AppDetail , versions [ ] string ) map [ string ] model . AppDetail {
appDetails := make ( map [ string ] model . AppDetail , len ( details ) )
for _ , old := range details {
old . Status = constant . AppTakeDown
appDetails [ old . Version ] = old
}
for _ , v := range versions {
detail , ok := appDetails [ v ]
if ok {
detail . Status = constant . AppNormal
appDetails [ v ] = detail
} else {
appDetails [ v ] = model . AppDetail {
Version : v ,
Status : constant . AppNormal ,
}
}
}
return appDetails
}
func getApps ( oldApps [ ] model . App , items [ ] dto . AppDefine ) map [ string ] model . App {
apps := make ( map [ string ] model . App , len ( oldApps ) )
for _ , old := range oldApps {
old . Status = constant . AppTakeDown
apps [ old . Key ] = old
}
for _ , item := range items {
app , ok := apps [ item . Key ]
if ! ok {
app = model . App { }
}
app . Name = item . Name
2022-11-22 14:32:45 +08:00
app . Limit = item . Limit
2022-10-10 15:10:53 +08:00
app . Key = item . Key
app . ShortDesc = item . ShortDesc
app . Author = item . Author
app . Source = item . Source
app . Type = item . Type
app . CrossVersionUpdate = item . CrossVersionUpdate
app . Required = item . GetRequired ( )
app . Status = constant . AppNormal
apps [ item . Key ] = app
}
return apps
}
2022-10-11 16:27:58 +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 )
}
2022-10-13 16:46:38 +08:00
_ = appInstallRepo . Save ( & install )
2022-10-11 16:27:58 +08:00
return reErr
}
2022-10-14 14:48:55 +08:00
func getAppFromOss ( ) error {
res , err := http . Get ( global . CONF . System . AppOss )
if err != nil {
return err
}
appByte , err := ioutil . ReadAll ( res . Body )
if err != nil {
return err
}
var ossConfig dto . AppOssConfig
if err := json . Unmarshal ( appByte , & ossConfig ) ; err != nil {
return err
}
appDir := constant . AppResourceDir
2022-11-18 18:38:52 +08:00
content , _ := os . ReadFile ( path . Join ( appDir , "apps.json" ) )
if content != nil {
oldConfig := & dto . AppOssConfig { }
if err := json . Unmarshal ( content , oldConfig ) ; err != nil {
return err
}
if oldConfig . Version == ossConfig . Version {
return nil
}
2022-10-14 14:48:55 +08:00
}
fileOp := files . NewFileOp ( )
if _ , err := fileOp . Backup ( appDir ) ; err != nil {
return err
}
2022-11-18 18:38:52 +08:00
packagePath := path . Join ( constant . ResourceDir , path . Base ( ossConfig . Package ) )
2022-10-14 14:48:55 +08:00
if err := fileOp . DownloadFile ( ossConfig . Package , packagePath ) ; err != nil {
return err
}
if err := fileOp . Decompress ( packagePath , constant . ResourceDir , files . Zip ) ; err != nil {
return err
}
defer func ( ) {
_ = fileOp . DeleteFile ( packagePath )
} ( )
return nil
}
2022-11-22 14:22:25 +08:00
func handleInstalled ( installed [ ] model . AppInstall ) ( [ ] dto . AppInstalled , error ) {
var res [ ] dto . AppInstalled
for _ , installed := range installed {
installDTO := dto . AppInstalled {
AppInstall : installed ,
}
app , err := appRepo . GetFirst ( commonRepo . WithByID ( installed . AppId ) )
if err != nil {
return nil , err
}
details , err := appDetailRepo . GetBy ( appDetailRepo . WithAppId ( app . ID ) )
if err != nil {
return nil , err
}
var versions [ ] string
for _ , detail := range details {
versions = append ( versions , detail . Version )
}
versions = common . GetSortedVersions ( versions )
lastVersion := versions [ 0 ]
if common . IsCrossVersion ( installed . Version , lastVersion ) {
installDTO . CanUpdate = app . CrossVersionUpdate
} else {
installDTO . CanUpdate = common . CompareVersion ( lastVersion , installed . Version )
}
res = append ( res , installDTO )
}
return res , nil
}
2022-11-24 10:28:39 +08:00
func getAppInstallByKey ( key string ) ( model . AppInstall , error ) {
app , err := appRepo . GetFirst ( appRepo . WithKey ( key ) )
if err != nil {
return model . AppInstall { } , err
}
appInstall , err := appInstallRepo . GetFirst ( appInstallRepo . WithAppId ( app . ID ) )
if err != nil {
return model . AppInstall { } , err
}
return appInstall , nil
}