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-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-10-20 16:15:11 +08:00
func execDockerCommand ( database model . AppDatabase , 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
dbConfig . DbName = database . Dbname
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-10-10 15:10:53 +08:00
_ , err := cmd . Exec ( getSqlStr ( database . Key , dbInstall . Version , op , execConfig ) )
2022-10-10 15:10:53 +08:00
if err != nil {
return err
}
return nil
}
2022-10-10 15:10:53 +08:00
func getSqlStr ( key , version string , operate DatabaseOp , exec dto . ContainerExec ) string {
2022-10-10 15:10:53 +08:00
var str string
param := exec . DbParam
switch key {
case "mysql" :
if operate == Add {
2022-10-10 15:10:53 +08:00
if common . CompareVersion ( version , "8.0" ) {
str = fmt . Sprintf ( "docker exec -i %s mysql -uroot -p%s -e \"CREATE USER '%s'@'%%' IDENTIFIED BY '%s';\" -e \"create database %s;\" -e \"GRANT ALL ON %s.* TO '%s'@'%%';\" -e \"FLUSH PRIVILEGES;\"" ,
exec . ContainerName , exec . Auth . RootPassword , param . DbUser , param . Password , param . DbName , param . DbName , param . DbUser )
} else {
str = fmt . Sprintf ( "docker exec -i %s mysql -uroot -p%s -e \"CREATE USER '%s'@'%%' IDENTIFIED BY '%s';\" -e \"create database %s;\" -e \"GRANT ALL ON %s.* TO '%s'@'%%' IDENTIFIED BY '%s';\" -e \"FLUSH PRIVILEGES;\"" ,
exec . ContainerName , exec . Auth . RootPassword , param . DbUser , param . Password , param . DbName , param . DbName , param . DbUser , param . Password )
}
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 )
}
}
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 ) {
return portN , errors . New ( "port is in used" )
} 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-10-20 16:15:11 +08:00
var database model . AppDatabase
2022-10-11 16:27:58 +08:00
database . Dbname = dbConfig . DbName
database . Username = dbConfig . DbUser
database . Password = dbConfig . Password
database . AppInstallId = dbInstall . ID
database . Key = dbInstall . App . Key
if err := dataBaseRepo . Create ( ctx , & database ) ; err != nil {
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-10-17 16:32:31 +08:00
func deleteAppInstall ( install model . AppInstall ) error {
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
}
}
tx , ctx := getTxAndContext ( )
if err := appInstallRepo . Delete ( ctx , install ) ; err != nil {
tx . Rollback ( )
return err
}
if err := deleteLink ( ctx , & install ) ; err != nil {
tx . Rollback ( )
return err
}
if err := appInstallBackupRepo . Delete ( ctx , appInstallBackupRepo . WithAppInstallID ( install . ID ) ) ; err != nil {
tx . Rollback ( )
return err
}
tx . Commit ( )
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" {
database , _ := dataBaseRepo . GetFirst ( commonRepo . WithByID ( re . ResourceId ) )
2022-10-20 16:15:11 +08:00
if reflect . DeepEqual ( database , model . AppDatabase { } ) {
2022-10-11 16:27:58 +08:00
continue
}
appInstall , err := appInstallRepo . GetFirst ( commonRepo . WithByID ( database . AppInstallId ) )
if err != nil {
return nil
}
if err := execDockerCommand ( database , appInstall , Delete ) ; err != nil {
return err
}
if err := dataBaseRepo . DeleteBy ( ctx , commonRepo . WithByID ( database . ID ) ) ; err != nil {
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
}
2022-10-17 16:32:31 +08:00
oldDetail , err := appDetailRepo . GetFirst ( commonRepo . WithByID ( install . AppDetailId ) )
if err != nil {
return err
}
2022-10-13 18:56:53 +08:00
detail , err := appDetailRepo . GetFirst ( commonRepo . WithByID ( detailId ) )
if err != nil {
return err
}
2022-10-17 16:32:31 +08:00
if oldDetail . LastVersion == detail . Version {
install . CanUpdate = false
}
2022-10-13 18:56:53 +08:00
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
if err := appInstallBackupRepo . Create ( ctx , backup ) ; err != nil {
return err
}
return nil
}
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-10-12 18:57:22 +08:00
resourceDir := path . Join ( constant . AppResourceDir , key , version )
installDir := path . Join ( constant . AppInstallDir , key )
2022-10-10 15:10:53 +08:00
installVersionDir := path . Join ( installDir , version )
fileOp := files . NewFileOp ( )
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
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
oldListFile := path . Join ( appDir , "list.json" )
content , err := os . ReadFile ( oldListFile )
if err != nil {
return err
}
list := & dto . AppList { }
if err := json . Unmarshal ( content , list ) ; err != nil {
return err
}
if list . Version == ossConfig . Version {
return nil
}
fileOp := files . NewFileOp ( )
if _ , err := fileOp . Backup ( appDir ) ; err != nil {
return err
}
packageName := path . Base ( ossConfig . Package )
packagePath := path . Join ( constant . ResourceDir , packageName )
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
}