From 325bb7bb5f8009ca02b5b727ebcdbd8f565f2003 Mon Sep 17 00:00:00 2001 From: ssongliu Date: Tue, 25 Oct 2022 18:34:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20mysql=20=E4=B8=8E=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E5=95=86=E5=BA=97=E8=81=94=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/databse_mysql.go | 54 +++- backend/app/dto/database.go | 19 +- backend/app/repo/databse_mysql.go | 87 ++++++ backend/app/service/database_mysql.go | 281 ++++++++++++------ backend/app/service/database_test.go | 100 +++---- backend/constant/container.go | 3 + backend/router/ro_database.go | 6 +- frontend/src/api/interface/database.ts | 12 + frontend/src/api/modules/database.ts | 18 +- frontend/src/lang/modules/zh.ts | 1 + frontend/src/views/database/mysql/index.vue | 49 ++- .../views/database/mysql/setting/index.vue | 103 ++++--- frontend/src/views/setting/index.vue | 2 +- 13 files changed, 520 insertions(+), 215 deletions(-) diff --git a/backend/app/api/v1/databse_mysql.go b/backend/app/api/v1/databse_mysql.go index 2771ed7c5..d342698df 100644 --- a/backend/app/api/v1/databse_mysql.go +++ b/backend/app/api/v1/databse_mysql.go @@ -1,6 +1,8 @@ package v1 import ( + "errors" + "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/constant" @@ -60,7 +62,7 @@ func (b *BaseApi) UpdateMysqlVariables(c *gin.Context) { } func (b *BaseApi) SearchMysql(c *gin.Context) { - var req dto.SearchWithPage + var req dto.SearchDBWithPage if err := c.ShouldBindJSON(&req); err != nil { helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) return @@ -88,16 +90,53 @@ func (b *BaseApi) DeleteMysql(c *gin.Context) { helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) return } + version, ok := c.Params.Get("version") + if !ok { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error version in path")) + return + } - if err := mysqlService.Delete(req.Ids); err != nil { + if err := mysqlService.Delete(version, req.Ids); err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return } helper.SuccessWithData(c, nil) } +func (b *BaseApi) LoadVersions(c *gin.Context) { + data, err := mysqlService.LoadRunningVersion() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + +func (b *BaseApi) LoadBaseinfo(c *gin.Context) { + version, ok := c.Params.Get("version") + if !ok { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error version in path")) + return + } + + data, err := mysqlService.LoadBaseInfo(version) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + + helper.SuccessWithData(c, data) +} + func (b *BaseApi) LoadStatus(c *gin.Context) { - data, err := mysqlService.LoadStatus("") + version, ok := c.Params.Get("version") + if !ok { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error version in path")) + return + } + + data, err := mysqlService.LoadStatus(version) if err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return @@ -106,8 +145,13 @@ func (b *BaseApi) LoadStatus(c *gin.Context) { helper.SuccessWithData(c, data) } -func (b *BaseApi) LoadConf(c *gin.Context) { - data, err := mysqlService.LoadVariables("") +func (b *BaseApi) LoadVariables(c *gin.Context) { + version, ok := c.Params.Get("version") + if !ok { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error version in path")) + return + } + data, err := mysqlService.LoadVariables(version) if err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return diff --git a/backend/app/dto/database.go b/backend/app/dto/database.go index 5a84e0a7b..e6be8c3f6 100644 --- a/backend/app/dto/database.go +++ b/backend/app/dto/database.go @@ -15,7 +15,7 @@ type MysqlDBInfo struct { type MysqlDBCreate struct { Name string `json:"name" validate:"required"` - Version string `json:"version" validate:"required,oneof=5.7.39 8.0.30"` + Version string `json:"version" validate:"required,oneof=mysql5.7 mysql8.0"` Format string `json:"format" validate:"required,oneof=utf8mb4 utf-8 gbk big5"` Username string `json:"username" validate:"required"` Password string `json:"password" validate:"required"` @@ -81,7 +81,7 @@ type MysqlVariables struct { } type MysqlVariablesUpdate struct { - Version string `json:"version" validate:"required"` + Version string `json:"version" validate:"required,oneof=mysql5.7 mysql8.0"` KeyBufferSize int64 `json:"key_buffer_size" validate:"required"` QueryCacheSize int64 `json:"query_cache_size" validate:"required"` TmpTableSize int64 `json:"tmp_table_size" validate:"required"` @@ -100,11 +100,20 @@ type MysqlVariablesUpdate struct { } type ChangeDBInfo struct { - ID uint `json:"id" validate:"required"` + ID uint `json:"id"` + Version string `json:"version" validate:"required,oneof=mysql5.7 mysql8.0"` Operation string `json:"operation" validate:"required,oneof=password privilege"` Value string `json:"value" validate:"required"` } -type BatchDeleteByName struct { - Names []string `json:"names" validate:"required"` +type DBBaseInfo struct { + Name string `json:"name"` + Port int64 `json:"port"` + Password string `json:"password"` + RemoteConn bool `json:"remoteConn"` +} + +type SearchDBWithPage struct { + PageInfo + Version string `json:"version" validate:"required"` } diff --git a/backend/app/repo/databse_mysql.go b/backend/app/repo/databse_mysql.go index 6c9149b4e..21f649747 100644 --- a/backend/app/repo/databse_mysql.go +++ b/backend/app/repo/databse_mysql.go @@ -1,19 +1,27 @@ package repo import ( + "encoding/json" + "errors" + "github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/global" + "gorm.io/gorm" ) type MysqlRepo struct{} type IMysqlRepo interface { Get(opts ...DBOption) (model.DatabaseMysql, error) + WithByVersion(version string) DBOption List(opts ...DBOption) ([]model.DatabaseMysql, error) Page(limit, offset int, opts ...DBOption) (int64, []model.DatabaseMysql, error) Create(mysql *model.DatabaseMysql) error Delete(opts ...DBOption) error Update(id uint, vars map[string]interface{}) error + LoadRunningVersion() ([]string, error) + LoadBaseInfoByVersion(key string) (*RootInfo, error) + UpdateMysqlConf(id uint, vars map[string]interface{}) error } func NewIMysqlRepo() IMysqlRepo { @@ -52,6 +60,72 @@ func (u *MysqlRepo) Page(page, size int, opts ...DBOption) (int64, []model.Datab return count, users, err } +func (u *MysqlRepo) LoadRunningVersion() ([]string, error) { + var ( + apps []model.App + appInstall model.AppInstall + results []string + ) + if err := global.DB.Where("name = ? OR name = ?", "Mysql5.7", "Mysql8.0").Find(&apps).Error; err != nil { + return nil, err + } + for _, app := range apps { + if err := global.DB.Where("app_id = ?", app.ID).First(&appInstall).Error; err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + if appInstall.ID != 0 { + results = append(results, app.Key) + } + } + return results, nil +} + +type RootInfo struct { + ID uint `json:"id"` + Name string `json:"name"` + Port int64 `json:"port"` + Password string `json:"password"` + ContainerName string `json:"containerName"` + Param string `json:"param"` + Env string `json:"env"` +} + +func (u *MysqlRepo) LoadBaseInfoByVersion(key string) (*RootInfo, error) { + var ( + app model.App + appInstall model.AppInstall + info RootInfo + ) + if err := global.DB.Where("key = ?", key).First(&app).Error; err != nil { + return nil, err + } + if err := global.DB.Where("app_id = ?", app.ID).First(&appInstall).Error; err != nil { + return nil, err + } + envMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(appInstall.Env), &envMap); err != nil { + return nil, err + } + password, ok := envMap["PANEL_DB_ROOT_PASSWORD"].(string) + if ok { + info.Password = password + } else { + return nil, errors.New("error password in db") + } + port, ok := envMap["PANEL_APP_PORT_HTTP"].(float64) + if ok { + info.Port = int64(port) + } else { + return nil, errors.New("error port in db") + } + info.ID = appInstall.ID + info.ContainerName = appInstall.ContainerName + info.Name = appInstall.Name + info.Env = appInstall.Env + info.Param = appInstall.Param + return &info, nil +} + func (u *MysqlRepo) Create(mysql *model.DatabaseMysql) error { return global.DB.Create(mysql).Error } @@ -67,3 +141,16 @@ func (u *MysqlRepo) Delete(opts ...DBOption) error { func (u *MysqlRepo) Update(id uint, vars map[string]interface{}) error { return global.DB.Model(&model.DatabaseMysql{}).Where("id = ?", id).Updates(vars).Error } + +func (u *MysqlRepo) UpdateMysqlConf(id uint, vars map[string]interface{}) error { + if err := global.DB.Model(&model.AppInstall{}).Where("id = ?", id).Updates(vars).Error; err != nil { + return err + } + return nil +} + +func (c *MysqlRepo) WithByVersion(version string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("version = ?", version) + } +} diff --git a/backend/app/service/database_mysql.go b/backend/app/service/database_mysql.go index 649df8742..5afafd68d 100644 --- a/backend/app/service/database_mysql.go +++ b/backend/app/service/database_mysql.go @@ -1,13 +1,15 @@ package service import ( - "database/sql" "encoding/json" "fmt" + "os/exec" "strconv" + "strings" "time" "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/go-sql-driver/mysql" "github.com/jinzhu/copier" @@ -17,46 +19,23 @@ import ( type MysqlService struct{} type IMysqlService interface { - SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error) + SearchWithPage(search dto.SearchDBWithPage) (int64, interface{}, error) Create(mysqlDto dto.MysqlDBCreate) error ChangeInfo(info dto.ChangeDBInfo) error UpdateVariables(variables dto.MysqlVariablesUpdate) error - Delete(ids []uint) error + Delete(version string, ids []uint) error LoadStatus(version string) (*dto.MysqlStatus, error) LoadVariables(version string) (*dto.MysqlVariables, error) + LoadRunningVersion() ([]string, error) + LoadBaseInfo(version string) (*dto.DBBaseInfo, error) } func NewIMysqlService() IMysqlService { return &MysqlService{} } -func newDatabaseClient() (*sql.DB, error) { - connArgs := fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8", "root", "Calong@2015", "localhost", 2306) - db, err := sql.Open("mysql", connArgs) - if err != nil { - return nil, err - } - return db, nil -} -func handleSql(db *sql.DB, query string) (map[string]string, error) { - variableMap := make(map[string]string) - rows, err := db.Query(query) - if err != nil { - return variableMap, err - } - - for rows.Next() { - var variableName, variableValue string - if err := rows.Scan(&variableName, &variableValue); err != nil { - return variableMap, err - } - variableMap[variableName] = variableValue - } - return variableMap, err -} - -func (u *MysqlService) SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error) { - total, mysqls, err := mysqlRepo.Page(search.Page, search.PageSize, commonRepo.WithLikeName(search.Info)) +func (u *MysqlService) SearchWithPage(search dto.SearchDBWithPage) (int64, interface{}, error) { + total, mysqls, err := mysqlRepo.Page(search.Page, search.PageSize, mysqlRepo.WithByVersion(search.Version)) var dtoMysqls []dto.MysqlDBInfo for _, mysql := range mysqls { var item dto.MysqlDBInfo @@ -68,6 +47,10 @@ func (u *MysqlService) SearchWithPage(search dto.SearchWithPage) (int64, interfa return total, dtoMysqls, err } +func (u *MysqlService) LoadRunningVersion() ([]string, error) { + return mysqlRepo.LoadRunningVersion() +} + func (u *MysqlService) Create(mysqlDto dto.MysqlDBCreate) error { if mysqlDto.Username == "root" { return errors.New("Cannot set root as user name") @@ -79,23 +62,23 @@ func (u *MysqlService) Create(mysqlDto dto.MysqlDBCreate) error { if err := copier.Copy(&mysql, &mysqlDto); err != nil { return errors.WithMessage(constant.ErrStructTransform, err.Error()) } - sql, err := newDatabaseClient() + + app, err := mysqlRepo.LoadBaseInfoByVersion(mysqlDto.Version) if err != nil { return err } - defer sql.Close() - if _, err := sql.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s CHARACTER SET=%s", mysqlDto.Name, mysqlDto.Format)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("create database if not exists %s character set=%s", mysqlDto.Name, mysqlDto.Format)); err != nil { return err } tmpPermission := mysqlDto.Permission - if _, err := sql.Exec(fmt.Sprintf("CREATE USER '%s'@'%s' IDENTIFIED BY '%s';", mysqlDto.Name, tmpPermission, mysqlDto.Password)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("create user if not exists '%s'@'%s' identified by '%s';", mysqlDto.Name, tmpPermission, mysqlDto.Password)); err != nil { return err } - grantStr := fmt.Sprintf("GRANT ALL PRIVILEGES ON %s.* TO '%s'@'%s'", mysqlDto.Name, mysqlDto.Username, tmpPermission) + grantStr := fmt.Sprintf("grant all privileges on %s.* to '%s'@'%s'", mysqlDto.Name, mysqlDto.Username, tmpPermission) if mysqlDto.Version == "5.7.39" { - grantStr = fmt.Sprintf("%s IDENTIFIED BY '%s' WITH GRANT OPTION;", grantStr, mysqlDto.Password) + grantStr = fmt.Sprintf("%s identified by '%s' with grant option;", grantStr, mysqlDto.Password) } - if _, err := sql.Exec(grantStr); err != nil { + if err := excuteSql(app.ContainerName, app.Password, grantStr); err != nil { return err } if err := mysqlRepo.Create(&mysql); err != nil { @@ -104,12 +87,12 @@ func (u *MysqlService) Create(mysqlDto dto.MysqlDBCreate) error { return nil } -func (u *MysqlService) Delete(ids []uint) error { - dbClient, err := newDatabaseClient() +func (u *MysqlService) Delete(version string, ids []uint) error { + app, err := mysqlRepo.LoadBaseInfoByVersion(version) if err != nil { return err } - defer dbClient.Close() + dbs, err := mysqlRepo.List(commonRepo.WithIdsIn(ids)) if err != nil { return err @@ -117,10 +100,10 @@ func (u *MysqlService) Delete(ids []uint) error { for _, db := range dbs { if len(db.Name) != 0 { - if _, err := dbClient.Exec(fmt.Sprintf("DROP USER IF EXISTS '%s'@'%s'", db.Name, db.Permission)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("drop user if exists '%s'@'%s'", db.Name, db.Permission)); err != nil { return err } - if _, err := dbClient.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", db.Name)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("drop database if exists %s", db.Name)); err != nil { return err } } @@ -130,103 +113,165 @@ func (u *MysqlService) Delete(ids []uint) error { } func (u *MysqlService) ChangeInfo(info dto.ChangeDBInfo) error { - mysql, err := mysqlRepo.Get(commonRepo.WithByID(info.ID)) - if err != nil { - return err - } - db, err := newDatabaseClient() - if err != nil { - return err - } - defer db.Close() - if info.Operation == "password" { - if _, err := db.Exec(fmt.Sprintf("SET PASSWORD FOR %s@%s = password('%s')", mysql.Username, mysql.Permission, info.Value)); err != nil { + var ( + mysql model.DatabaseMysql + err error + ) + if info.ID != 0 { + mysql, err = mysqlRepo.Get(commonRepo.WithByID(info.ID)) + if err != nil { return err } - _ = mysqlRepo.Update(mysql.ID, map[string]interface{}{"password": info.Value}) + } + app, err := mysqlRepo.LoadBaseInfoByVersion(info.Version) + if err != nil { + return err + } + if info.Operation == "password" { + if info.ID != 0 { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set password for %s@%s = password('%s')", mysql.Username, mysql.Permission, info.Value)); err != nil { + return err + } + _ = mysqlRepo.Update(mysql.ID, map[string]interface{}{"password": info.Value}) + return nil + } + hosts, err := excuteSqlForRows(app.ContainerName, app.Password, "select host from mysql.user where user='root';") + if err != nil { + return err + } + for _, host := range hosts { + if host == "%" || host == "localhost" { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set password for root@'%s' = password('%s')", host, info.Value)); err != nil { + return err + } + } + } + _ = mysqlRepo.UpdateMysqlConf(app.ID, map[string]interface{}{ + "param": strings.ReplaceAll(app.Param, app.Password, info.Value), + "env": strings.ReplaceAll(app.Env, app.Password, info.Value), + }) return nil } - if _, err := db.Exec(fmt.Sprintf("DROP USER IF EXISTS '%s'@'%s'", mysql.Name, mysql.Permission)); err != nil { + if info.ID == 0 { + mysql.Name = "*" + mysql.Username = "root" + mysql.Permission = "%" + mysql.Password = app.Password + } + + if info.Value != mysql.Permission { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("drop user if exists '%s'@'%s'", mysql.Username, mysql.Permission)); err != nil { + return err + } + if info.ID == 0 { + return nil + } + } + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("create user if not exists '%s'@'%s' identified by '%s';", mysql.Username, info.Value, mysql.Password)); err != nil { return err } - grantStr := fmt.Sprintf("GRANT ALL PRIVILEGES ON %s.* TO '%s'@'%s'", mysql.Name, mysql.Username, info.Value) + grantStr := fmt.Sprintf("grant all privileges on %s.* to '%s'@'%s'", mysql.Name, mysql.Username, info.Value) if mysql.Version == "5.7.39" { - grantStr = fmt.Sprintf("%s IDENTIFIED BY '%s' WITH GRANT OPTION;", grantStr, mysql.Password) + grantStr = fmt.Sprintf("%s identified by '%s' with grant option;", grantStr, mysql.Password) } - if _, err := db.Exec(grantStr); err != nil { + if err := excuteSql(app.ContainerName, app.Password, grantStr); err != nil { return err } - if _, err := db.Exec("FLUSH PRIVILEGES"); err != nil { + if err := excuteSql(app.ContainerName, app.Password, "flush privileges"); err != nil { return err } + if info.ID == 0 { + return nil + } + _ = mysqlRepo.Update(mysql.ID, map[string]interface{}{"permission": info.Value}) return nil } func (u *MysqlService) UpdateVariables(variables dto.MysqlVariablesUpdate) error { - db, err := newDatabaseClient() + app, err := mysqlRepo.LoadBaseInfoByVersion(variables.Version) if err != nil { return err } - defer db.Close() - if _, err := db.Exec(fmt.Sprintf("SET GLOBAL key_buffer_size=%d", variables.KeyBufferSize)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set GLOBAL key_buffer_size=%d", variables.KeyBufferSize)); err != nil { return err } - if _, err := db.Exec(fmt.Sprintf("SET GLOBAL query_cache_size=%d", variables.QueryCacheSize)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set GLOBAL query_cache_size=%d", variables.QueryCacheSize)); err != nil { return err } - if _, err := db.Exec(fmt.Sprintf("SET GLOBAL tmp_table_size=%d", variables.TmpTableSize)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set GLOBAL tmp_table_size=%d", variables.TmpTableSize)); err != nil { return err } - if _, err := db.Exec(fmt.Sprintf("SET GLOBAL innodb_buffer_pool_size=%d", variables.InnodbBufferPoolSize)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set GLOBAL innodb_buffer_pool_size=%d", variables.InnodbBufferPoolSize)); err != nil { return err } - // if _, err := db.Exec(fmt.Sprintf("SET GLOBAL innodb_log_buffer_size=%d", variables.InnodbLogBufferSize)); err != nil { + // if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set GLOBAL innodb_log_buffer_size=%d", variables.InnodbLogBufferSize)); err != nil { // return err // } - if _, err := db.Exec(fmt.Sprintf("SET GLOBAL sort_buffer_size=%d", variables.SortBufferSize)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set GLOBAL sort_buffer_size=%d", variables.SortBufferSize)); err != nil { return err } - if _, err := db.Exec(fmt.Sprintf("SET GLOBAL read_buffer_size=%d", variables.ReadBufferSize)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set GLOBAL read_buffer_size=%d", variables.ReadBufferSize)); err != nil { return err } - if _, err := db.Exec(fmt.Sprintf("SET GLOBAL read_rnd_buffer_size=%d", variables.ReadRndBufferSize)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set GLOBAL read_rnd_buffer_size=%d", variables.ReadRndBufferSize)); err != nil { return err } - if _, err := db.Exec(fmt.Sprintf("SET GLOBAL join_buffer_size=%d", variables.JoinBufferSize)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set GLOBAL join_buffer_size=%d", variables.JoinBufferSize)); err != nil { return err } - // if _, err := db.Exec(fmt.Sprintf("SET GLOBAL thread_stack=%d", variables.ThreadStack)); err != nil { + // if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set GLOBAL thread_stack=%d", variables.ThreadStack)); err != nil { // return err // } - if _, err := db.Exec(fmt.Sprintf("SET GLOBAL binlog_cache_size=%d", variables.BinlogCachSize)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set GLOBAL binlog_cache_size=%d", variables.BinlogCachSize)); err != nil { return err } - if _, err := db.Exec(fmt.Sprintf("SET GLOBAL thread_cache_size=%d", variables.ThreadCacheSize)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set GLOBAL thread_cache_size=%d", variables.ThreadCacheSize)); err != nil { return err } - if _, err := db.Exec(fmt.Sprintf("SET GLOBAL table_open_cache=%d", variables.TableOpenCache)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set GLOBAL table_open_cache=%d", variables.TableOpenCache)); err != nil { return err } - if _, err := db.Exec(fmt.Sprintf("SET GLOBAL max_connections=%d", variables.MaxConnections)); err != nil { + if err := excuteSql(app.ContainerName, app.Password, fmt.Sprintf("set GLOBAL max_connections=%d", variables.MaxConnections)); err != nil { return err } return nil } -func (u *MysqlService) LoadVariables(version string) (*dto.MysqlVariables, error) { - db, err := newDatabaseClient() +func (u *MysqlService) LoadBaseInfo(version string) (*dto.DBBaseInfo, error) { + var data dto.DBBaseInfo + app, err := mysqlRepo.LoadBaseInfoByVersion(version) if err != nil { return nil, err } - defer db.Close() + data.Name = app.Name + data.Port = int64(app.Port) + data.Password = app.Password - variableMap, err := handleSql(db, "SHOW VARIABLES") + hosts, err := excuteSqlForRows(app.ContainerName, app.Password, "select host from mysql.user where user='root';") + if err != nil { + return nil, err + } + for _, host := range hosts { + if host == "%" { + data.RemoteConn = true + break + } + } + return &data, nil +} + +func (u *MysqlService) LoadVariables(version string) (*dto.MysqlVariables, error) { + app, err := mysqlRepo.LoadBaseInfoByVersion(version) + if err != nil { + return nil, err + } + variableMap, err := excuteSqlForMaps(app.ContainerName, app.Password, "show global variables;") if err != nil { return nil, err } @@ -240,51 +285,89 @@ func (u *MysqlService) LoadVariables(version string) (*dto.MysqlVariables, error } func (u *MysqlService) LoadStatus(version string) (*dto.MysqlStatus, error) { - db, err := newDatabaseClient() + app, err := mysqlRepo.LoadBaseInfoByVersion(version) if err != nil { return nil, err } - defer db.Close() - globalMap, err := handleSql(db, "SHOW GLOBAL STATUS") + statusMap, err := excuteSqlForMaps(app.ContainerName, app.Password, "show global status;") if err != nil { return nil, err } + var info dto.MysqlStatus - arr, err := json.Marshal(globalMap) + arr, err := json.Marshal(statusMap) if err != nil { return nil, err } _ = json.Unmarshal(arr, &info) - if value, ok := globalMap["Run"]; ok { + if value, ok := statusMap["Run"]; ok { uptime, _ := strconv.Atoi(value) info.Run = time.Unix(time.Now().Unix()-int64(uptime), 0).Format("2006-01-02 15:04:05") } else { - if value, ok := globalMap["Uptime"]; ok { + if value, ok := statusMap["Uptime"]; ok { uptime, _ := strconv.Atoi(value) info.Run = time.Unix(time.Now().Unix()-int64(uptime), 0).Format("2006-01-02 15:04:05") } } - rows, err := db.Query("SHOW MASTER STATUS") + info.File = "OFF" + info.Position = "OFF" + rows, err := excuteSqlForRows(app.ContainerName, app.Password, "show master status;") if err != nil { - return &info, err + return nil, err } - masterRows := make([]string, 5) - for rows.Next() { - if err := rows.Scan(&masterRows[0], &masterRows[1], &masterRows[2], &masterRows[3], &masterRows[4]); err != nil { - return &info, err + if len(rows) > 2 { + itemValue := strings.Split(rows[1], "\t") + if len(itemValue) > 2 { + info.File = itemValue[0] + info.Position = itemValue[1] } } - info.File = masterRows[0] - if len(masterRows[0]) == 0 { - info.File = "OFF" - } - info.Position = masterRows[1] - if len(masterRows[1]) == 0 { - info.Position = "OFF" - } return &info, nil } + +func excuteSqlForMaps(containerName, password, command string) (map[string]string, error) { + cmd := exec.Command("docker", "exec", "-i", containerName, "mysql", "-uroot", fmt.Sprintf("-p%s", password), "-e", command) + stdout, err := cmd.CombinedOutput() + if err != nil { + return nil, err + } + + stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") + rows := strings.Split(stdStr, "\n") + rowMap := make(map[string]string) + for _, v := range rows { + itemRow := strings.Split(v, "\t") + if len(itemRow) == 2 { + rowMap[itemRow[0]] = itemRow[1] + } + } + return rowMap, nil +} + +func excuteSqlForRows(containerName, password, command string) ([]string, error) { + cmd := exec.Command("docker", "exec", "-i", containerName, "mysql", "-uroot", fmt.Sprintf("-p%s", password), "-e", command) + stdout, err := cmd.CombinedOutput() + if err != nil { + return nil, err + } + stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") + return strings.Split(stdStr, "\n"), nil +} + +func excuteSql(containerName, password, command string) error { + cmd := exec.Command("docker", "exec", "-i", containerName, "mysql", "-uroot", fmt.Sprintf("-p%s", password), "-e", command) + stdout, err := cmd.CombinedOutput() + if err != nil { + return err + } + stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") + + if strings.HasPrefix(string(stdStr), "ERROR ") { + return errors.New(string(stdStr)) + } + return nil +} diff --git a/backend/app/service/database_test.go b/backend/app/service/database_test.go index 223b04946..8656f92c1 100644 --- a/backend/app/service/database_test.go +++ b/backend/app/service/database_test.go @@ -1,9 +1,10 @@ package service import ( - "database/sql" + "encoding/json" "fmt" - "reflect" + "os/exec" + "strings" "testing" "github.com/1Panel-dev/1Panel/backend/app/dto" @@ -11,61 +12,60 @@ import ( ) func TestMysql(t *testing.T) { - connArgs := fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8", "root", "Calong@2015", "172.16.10.143", 3306) - db, err := sql.Open("mysql", connArgs) + cmd := exec.Command("docker", "exec", "-i", "1Panel-mysql5.7-RnzE", "mysql", "-uroot", "-pCalong@2016", "-e", "show global variables;") + stdout, err := cmd.CombinedOutput() if err != nil { fmt.Println(err) } - defer db.Close() - - rows, err := db.Query("show variables") - if err != nil { - fmt.Println(err) - } - variableMap := make(map[string]int) - - for rows.Next() { - var variableName string - var variableValue int - if err := rows.Scan(&variableName, &variableValue); err != nil { - fmt.Println(err) + kk := strings.Split(string(stdout), "\n") + testMap := make(map[string]interface{}) + for _, v := range kk { + itemRow := strings.Split(v, "\t") + if len(itemRow) == 2 { + testMap[itemRow[0]] = itemRow[1] } - variableMap[variableName] = variableValue } - for k, v := range variableMap { - fmt.Println(k, v) - } -} - -func TestMs(t *testing.T) { - db, err := newDatabaseClient() + var info dto.MysqlVariables + arr, err := json.Marshal(testMap) if err != nil { fmt.Println(err) } - defer db.Close() + _ = json.Unmarshal(arr, &info) + fmt.Print(info) + // fmt.Println(string(stdout)) + // for { + // str, err := hr.Reader.ReadString('\n') + // if err == nil { + // testMap := make(map[string]interface{}) + // err = json.Unmarshal([]byte(str), &testMap) + // fmt.Println(err) + // for k, v := range testMap { + // fmt.Println(k, v) + // } + // // fmt.Print(str) + // } else if err == io.EOF { + // // ReadString最后会同EOF和最后的数据一起返回 + // fmt.Println(str) + // break + // } else { + // fmt.Println("出错!!") + // return + // } + // } + // input, err := hr.Reader.ReadString('\n') + // if err == nil { + // fmt.Printf("The input was: %s\n", input) + // } - variables := dto.MysqlVariablesUpdate{ - Version: "5.7.39", - KeyBufferSize: 8388608, - QueryCacheSize: 1048576, - TmpTableSize: 16777216, - InnodbBufferPoolSize: 134217728, - InnodbLogBufferSize: 16777216, - SortBufferSize: 262144, - ReadBufferSize: 131072, - - ReadRndBufferSize: 262144, - JoinBufferSize: 262144, - ThreadStack: 262144, - BinlogCachSize: 32768, - ThreadCacheSize: 9, - TableOpenCache: 2000, - MaxConnections: 150, - } - - v := reflect.ValueOf(variables) - typeOfS := v.Type() - for i := 0; i < v.NumField(); i++ { - fmt.Printf("SET GLOBAL %s=%d \n", typeOfS.Field(i).Name, v.Field(i).Interface()) - } + // _, err = hr.Conn.Write([]byte("show global variables; \n")) + // if err != nil { + // fmt.Println(err) + // } + // time.Sleep(3 * time.Second) + // buf1 := make([]byte, 1024) + // _, err = hr.Reader.Read(buf1) + // if err != nil { + // fmt.Println(err) + // } + // fmt.Println(string(buf1)) } diff --git a/backend/constant/container.go b/backend/constant/container.go index 353179268..d668b425b 100644 --- a/backend/constant/container.go +++ b/backend/constant/container.go @@ -17,4 +17,7 @@ const ( DaemonJsonDir = "/System/Volumes/Data/Users/slooop/.docker/daemon.json" TmpDockerBuildDir = "/opt/1Panel/data/docker/build" TmpComposeBuildDir = "/opt/1Panel/data/docker/compose" + + ExecCmd = "docker exec" + ExecCmdIT = "docker exec -it" ) diff --git a/backend/router/ro_database.go b/backend/router/ro_database.go index 60e176d1b..7dd9fa218 100644 --- a/backend/router/ro_database.go +++ b/backend/router/ro_database.go @@ -26,7 +26,9 @@ func (s *DatabaseRouter) InitDatabaseRouter(Router *gin.RouterGroup) { withRecordRouter.POST("/del", baseApi.DeleteMysql) withRecordRouter.POST("/variables/update", baseApi.UpdateMysqlVariables) cmdRouter.POST("/search", baseApi.SearchMysql) - cmdRouter.GET("/conf", baseApi.LoadConf) - cmdRouter.GET("/status", baseApi.LoadStatus) + cmdRouter.GET("/variables/:version", baseApi.LoadVariables) + cmdRouter.GET("/status/:version", baseApi.LoadStatus) + cmdRouter.GET("/baseinfo/:version", baseApi.LoadBaseinfo) + cmdRouter.GET("/versions", baseApi.LoadVersions) } } diff --git a/frontend/src/api/interface/database.ts b/frontend/src/api/interface/database.ts index cbea672a1..dbbdee428 100644 --- a/frontend/src/api/interface/database.ts +++ b/frontend/src/api/interface/database.ts @@ -1,4 +1,9 @@ +import { ReqPage } from '.'; + export namespace Database { + export interface Search extends ReqPage { + version: string; + } export interface MysqlDBInfo { id: number; createdAt: Date; @@ -9,6 +14,12 @@ export namespace Database { permission: string; description: string; } + export interface BaseInfo { + name: string; + port: number; + password: string; + remoteConn: boolean; + } export interface MysqlDBCreate { name: string; version: string; @@ -74,6 +85,7 @@ export namespace Database { } export interface ChangeInfo { id: number; + version: string; operation: string; value: string; } diff --git a/frontend/src/api/modules/database.ts b/frontend/src/api/modules/database.ts index 9c0693f90..aec26eaa5 100644 --- a/frontend/src/api/modules/database.ts +++ b/frontend/src/api/modules/database.ts @@ -1,8 +1,8 @@ import http from '@/api'; -import { ResPage, ReqPage } from '../interface'; +import { ResPage } from '../interface'; import { Database } from '../interface/database'; -export const searchMysqlDBs = (params: ReqPage) => { +export const searchMysqlDBs = (params: Database.Search) => { return http.post>(`databases/search`, params); }; @@ -19,9 +19,15 @@ export const deleteMysqlDB = (params: { ids: number[] }) => { return http.post(`/databases/del`, params); }; -export const loadMysqlVariables = () => { - return http.get(`/databases/conf`); +export const loadMysqlBaseInfo = (param: string) => { + return http.get(`/databases/baseinfo/${param}`); }; -export const loadMysqlStatus = () => { - return http.get(`/databases/status`); +export const loadMysqlVariables = (param: string) => { + return http.get(`/databases/variables/${param}`); +}; +export const loadMysqlStatus = (param: string) => { + return http.get(`/databases/status/${param}`); +}; +export const loadVersions = () => { + return http.get(`/databases/versions`); }; diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index f9522344d..0fdc7e8c6 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -164,6 +164,7 @@ export default { changePassword: '改密', baseSetting: '基础设置', + remoteConnHelper: 'root 帐号远程连接 mysql 有安全风险,开启需谨慎!', confChange: '配置修改', currentStatus: '当前状态', diff --git a/frontend/src/views/database/mysql/index.vue b/frontend/src/views/database/mysql/index.vue index 837b49fab..17305f20b 100644 --- a/frontend/src/views/database/mysql/index.vue +++ b/frontend/src/views/database/mysql/index.vue @@ -2,11 +2,12 @@
- Mysql 版本 {{ version }} + {{ version }} @@ -116,16 +117,16 @@ import Setting from '@/views/database/mysql/setting/index.vue'; import Submenu from '@/views/database/index.vue'; import { dateFromat } from '@/utils/util'; import { onMounted, reactive, ref } from 'vue'; -import { deleteMysqlDB, searchMysqlDBs, updateMysqlDBInfo } from '@/api/modules/database'; +import { deleteMysqlDB, loadVersions, searchMysqlDBs, updateMysqlDBInfo } from '@/api/modules/database'; import i18n from '@/lang'; -import { Cronjob } from '@/api/interface/cronjob'; import { useDeleteData } from '@/hooks/use-delete-data'; import { ElForm, ElMessage } from 'element-plus'; import { Database } from '@/api/interface/database'; import { Rules } from '@/global/form-rules'; const selects = ref([]); -const version = ref('5.7.39'); +const mysqlVersions = ref(); +const version = ref('5.7'); const isOnSetting = ref(); const data = ref(); @@ -162,6 +163,7 @@ type FormInstance = InstanceType; const changeFormRef = ref(); const changeForm = reactive({ id: 0, + version: '', userName: '', password: '', operation: '', @@ -170,11 +172,11 @@ const changeForm = reactive({ value: '', }); const submitChangeInfo = async (formEl: FormInstance | undefined) => { - console.log(formEl); if (!formEl) return; formEl.validate(async (valid) => { if (!valid) return; changeForm.value = changeForm.operation === 'password' ? changeForm.password : changeForm.privilege; + changeForm.version = version.value; await updateMysqlDBInfo(changeForm); ElMessage.success(i18n.global.t('commons.msg.operationSuccess')); search(); @@ -182,22 +184,43 @@ const submitChangeInfo = async (formEl: FormInstance | undefined) => { }); }; +const loadRunningOptions = async () => { + const res = await loadVersions(); + mysqlVersions.value = res.data; + if (mysqlVersions.value.length != 0) { + version.value = mysqlVersions.value[0]; + search(); + } +}; + +const onChangeVersion = async (val: string) => { + version.value = val; + search(); + if (isOnSetting.value) { + let params = { + version: version.value, + }; + settingRef.value!.acceptParams(params); + } +}; + const search = async () => { let params = { page: paginationConfig.currentPage, pageSize: paginationConfig.pageSize, + version: version.value, }; const res = await searchMysqlDBs(params); data.value = res.data.items || []; paginationConfig.total = res.data.total; }; -const onBatchDelete = async (row: Cronjob.CronjobInfo | null) => { +const onBatchDelete = async (row: Database.MysqlDBInfo | null) => { let ids: Array = []; if (row) { ids.push(row.id); } else { - selects.value.forEach((item: Cronjob.CronjobInfo) => { + selects.value.forEach((item: Database.MysqlDBInfo) => { ids.push(item.id); }); } @@ -230,25 +253,25 @@ const buttons = [ }, { label: i18n.global.t('database.backupList') + '(1)', - click: (row: Cronjob.CronjobInfo) => { + click: (row: Database.MysqlDBInfo) => { onBatchDelete(row); }, }, { label: i18n.global.t('database.loadBackup'), - click: (row: Cronjob.CronjobInfo) => { + click: (row: Database.MysqlDBInfo) => { onBatchDelete(row); }, }, { label: i18n.global.t('commons.button.delete'), - click: (row: Cronjob.CronjobInfo) => { + click: (row: Database.MysqlDBInfo) => { onBatchDelete(row); }, }, ]; onMounted(() => { - search(); + loadRunningOptions(); }); diff --git a/frontend/src/views/database/mysql/setting/index.vue b/frontend/src/views/database/mysql/setting/index.vue index d7c945bc2..db31b9f83 100644 --- a/frontend/src/views/database/mysql/setting/index.vue +++ b/frontend/src/views/database/mysql/setting/index.vue @@ -3,15 +3,15 @@ - +
- - + + - - + + - - - - + + + {{ $t('database.remoteConnHelper') }}
@@ -65,7 +68,7 @@ {{ $t('commons.button.save') }} @@ -317,7 +320,13 @@ import { javascript } from '@codemirror/lang-javascript'; import { oneDark } from '@codemirror/theme-one-dark'; import { LoadFile } from '@/api/modules/files'; import { planOptions } from './helper'; -import { loadMysqlStatus, loadMysqlVariables, updateMysqlVariables } from '@/api/modules/database'; +import { + loadMysqlBaseInfo, + loadMysqlStatus, + loadMysqlVariables, + updateMysqlDBInfo, + updateMysqlVariables, +} from '@/api/modules/database'; import { computeSize } from '@/utils/util'; import { Rules } from '@/global/form-rules'; import i18n from '@/lang'; @@ -325,16 +334,11 @@ import i18n from '@/lang'; const extensions = [javascript(), oneDark]; const activeName = ref('1'); -const form = reactive({ - port: '', +const baseInfo = reactive({ + name: '', + port: 3306, password: '', - remoteAccess: '', - sessionTimeout: 0, - localTime: '', - panelName: '', - theme: '', - language: '', - complexityVerification: '', + remoteConn: false, }); const panelFormRef = ref(); const mysqlConf = ref(); @@ -407,17 +411,48 @@ interface DialogProps { } const acceptParams = (params: DialogProps): void => { onSetting.value = true; - loadMysqlConf('/opt/1Panel/conf/mysql.conf'); + paramVersion.value = params.version; + loadBaseInfo(); loadStatus(); loadVariables(); - paramVersion.value = params.version; }; const onClose = (): void => { onSetting.value = false; }; const onSave = async (formEl: FormInstance | undefined, key: string, val: any) => { - console.log(formEl, key, val); + if (!formEl) return; + const result = await formEl.validateField(key, callback); + if (!result) { + return; + } + let changeForm = { + id: 0, + version: paramVersion.value, + value: val, + operation: key === 'remoteConn' ? 'privilege' : key, + }; + if (changeForm.operation === 'privilege') { + changeForm.value = val ? '%' : 'localhost'; + } + await updateMysqlDBInfo(changeForm); + ElMessage.success(i18n.global.t('commons.msg.operationSuccess')); +}; +function callback(error: any) { + if (error) { + return error.message; + } else { + return; + } +} + +const loadBaseInfo = async () => { + const res = await loadMysqlBaseInfo(paramVersion.value); + baseInfo.name = res.data?.name; + baseInfo.port = res.data?.port; + baseInfo.password = res.data?.password; + baseInfo.remoteConn = res.data?.remoteConn; + loadMysqlConf(`/opt/1Panel/data/apps/${paramVersion.value}/${baseInfo.name}/conf/my.cnf`); }; const loadMysqlConf = async (path: string) => { @@ -426,7 +461,7 @@ const loadMysqlConf = async (path: string) => { }; const loadVariables = async () => { - const res = await loadMysqlVariables(); + const res = await loadMysqlVariables(paramVersion.value); mysqlVariables.key_buffer_size = Number(res.data.key_buffer_size) / 1024 / 1024; mysqlVariables.query_cache_size = Number(res.data.query_cache_size) / 1024 / 1024; mysqlVariables.tmp_table_size = Number(res.data.tmp_table_size) / 1024 / 1024; @@ -494,7 +529,7 @@ const onSaveVariables = async (formEl: FormInstance | undefined) => { }; const loadStatus = async () => { - const res = await loadMysqlStatus(); + const res = await loadMysqlStatus(paramVersion.value); let queryPerSecond = res.data.Questions / res.data.Uptime; let txPerSecond = (res.data!.Com_commit + res.data.Com_rollback) / res.data.Uptime; diff --git a/frontend/src/views/setting/index.vue b/frontend/src/views/setting/index.vue index d566535d7..3c6e8ef79 100644 --- a/frontend/src/views/setting/index.vue +++ b/frontend/src/views/setting/index.vue @@ -78,7 +78,7 @@ const { switchDark } = useTheme(); const SaveSetting = async (formEl: FormInstance | undefined, key: string, val: any) => { if (!formEl) return; - const result = await formEl.validateField('settingInfo.' + key.replace(key[0], key[0].toLowerCase()), callback); + const result = await formEl.validateField('settingInfo.' + key[0].toLowerCase(), callback); if (!result) { return; }