mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
1577 lines
45 KiB
Go
1577 lines
45 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/mysql"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/mysql/client"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
|
_ "github.com/go-sql-driver/mysql"
|
|
"github.com/jinzhu/copier"
|
|
"github.com/pkg/errors"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type MysqlService struct{}
|
|
|
|
type IMysqlService interface {
|
|
SearchWithPage(search dto.MysqlDBSearch) (int64, interface{}, error)
|
|
ListDBOption() ([]dto.MysqlOption, error)
|
|
Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error)
|
|
LoadFromRemote(req dto.MysqlLoadDB) error
|
|
ChangeAccess(info dto.ChangeDBInfo) error
|
|
ChangePassword(info dto.ChangeDBInfo) error
|
|
UpdateVariables(req dto.MysqlVariablesUpdate) error
|
|
UpdateDescription(req dto.UpdateDescription) error
|
|
DeleteCheck(req dto.MysqlDBDeleteCheck) ([]dto.DBResource, error)
|
|
Delete(ctx context.Context, req dto.MysqlDBDelete) error
|
|
|
|
ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, error)
|
|
ListGrants(req dto.MysqlUserSearch) ([]dto.MysqlGrant, error)
|
|
ListGrantSummary(req dto.MysqlGrantSummarySearch) (map[string][]dto.MysqlUser, error)
|
|
CreateUser(req dto.MysqlUserCreate) error
|
|
UpdateUser(req dto.MysqlUserUpdate) error
|
|
ChangeUserPassword(req dto.MysqlUserPassword) error
|
|
SaveUserPassword(req dto.MysqlUserPassword) error
|
|
DeleteUser(req dto.MysqlUserDelete) error
|
|
GrantUser(req dto.MysqlGrantCreate) error
|
|
RevokeGrant(req dto.MysqlGrantDelete) error
|
|
|
|
LoadFormatOption(req dto.OperationWithName) []dto.MysqlFormatCollationOption
|
|
LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlStatus, error)
|
|
LoadVariables(req dto.OperationWithNameAndType) (*dto.MysqlVariables, error)
|
|
LoadRemoteAccess(req dto.OperationWithNameAndType) (bool, error)
|
|
}
|
|
|
|
func NewIMysqlService() IMysqlService {
|
|
return &MysqlService{}
|
|
}
|
|
|
|
func normalizeDatabaseUserType(dbType string) string {
|
|
if len(dbType) == 0 {
|
|
return "mysql"
|
|
}
|
|
return dbType
|
|
}
|
|
|
|
func resolveDatabaseUserType(database string) (string, error) {
|
|
databaseItem, err := databaseRepo.Get(repo.WithByName(database))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return normalizeDatabaseUserType(databaseItem.Type), nil
|
|
}
|
|
|
|
func databaseUserKey(username, host string) string {
|
|
return username + "@" + host
|
|
}
|
|
|
|
func splitMysqlHosts(permission string) []string {
|
|
hosts := strings.Split(permission, ",")
|
|
res := make([]string, 0, len(hosts))
|
|
seen := make(map[string]struct{}, len(hosts))
|
|
for _, host := range hosts {
|
|
host = strings.TrimSpace(host)
|
|
if len(host) == 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[host]; ok {
|
|
continue
|
|
}
|
|
seen[host] = struct{}{}
|
|
res = append(res, host)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func parseMysqlHosts(permission string, allowMultiple bool) ([]string, error) {
|
|
hosts := splitMysqlHosts(permission)
|
|
if len(hosts) == 0 {
|
|
return nil, errors.New("mysql user host is required")
|
|
}
|
|
if !allowMultiple && len(hosts) > 1 {
|
|
return nil, errors.New("multiple mysql user hosts are not supported for this operation")
|
|
}
|
|
return hosts, nil
|
|
}
|
|
|
|
func checkMysqlNormalUser(username string) error {
|
|
if isMysqlSystemUser(username) {
|
|
return errors.New("mysql system user does not support this operation")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// isMysqlSystemUser identifies internal accounts which must only be managed by
|
|
// MySQL or its container runtime. The root account is managed through the
|
|
// dedicated root API.
|
|
func isMysqlSystemUser(username string) bool {
|
|
switch strings.ToLower(username) {
|
|
case "root",
|
|
"mysql.session", "mysql.sys", "mysql.infoschema", "mysqlxsys",
|
|
"mariadb.sys", "mariadb-sys",
|
|
"debian-sys-maint",
|
|
"healthcheck":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func saveDatabaseUserCredential(dbType, database, username, host, password, description string) error {
|
|
dbType = normalizeDatabaseUserType(dbType)
|
|
user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database), databaseUserRepo.WithByUser(username, host))
|
|
if err != nil {
|
|
user = model.DatabaseUser{
|
|
Type: dbType,
|
|
Database: database,
|
|
Username: username,
|
|
Host: host,
|
|
}
|
|
}
|
|
user.Password = password
|
|
user.IsDelete = false
|
|
if len(description) != 0 {
|
|
user.Description = description
|
|
}
|
|
return databaseUserRepo.Save(&user)
|
|
}
|
|
|
|
func saveDatabaseUserCredentials(dbType, database, username, permission, password, description string) error {
|
|
for _, host := range splitMysqlHosts(permission) {
|
|
if err := saveDatabaseUserCredential(dbType, database, username, host, password, description); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type mysqlUserAppTarget struct {
|
|
Key string
|
|
Name string
|
|
}
|
|
|
|
func loadMysqlUserAppTargets(dbType, database, username, host string, dbNames ...string) ([]mysqlUserAppTarget, error) {
|
|
if host != "%" {
|
|
return nil, nil
|
|
}
|
|
dbNameSet := make(map[string]struct{}, len(dbNames))
|
|
for _, dbName := range dbNames {
|
|
dbNameSet[dbName] = struct{}{}
|
|
}
|
|
dbItems, err := mysqlRepo.List(mysqlRepo.WithByMysqlName(database))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
localResourceIDs := make([]uint, 0, len(dbItems))
|
|
remoteResourceIDs := make([]uint, 0, len(dbItems))
|
|
for _, dbItem := range dbItems {
|
|
if len(dbNameSet) != 0 {
|
|
if _, ok := dbNameSet[dbItem.Name]; !ok {
|
|
continue
|
|
}
|
|
}
|
|
if dbItem.ID == 0 {
|
|
continue
|
|
}
|
|
if dbItem.From == "local" {
|
|
localResourceIDs = append(localResourceIDs, dbItem.ID)
|
|
} else {
|
|
remoteResourceIDs = append(remoteResourceIDs, dbItem.ID)
|
|
}
|
|
}
|
|
|
|
appResources := make([]model.AppInstallResource, 0)
|
|
if len(localResourceIDs) != 0 {
|
|
app, err := appInstallRepo.LoadBaseInfo(dbType, database)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resources, err := appInstallResourceRepo.GetBy(
|
|
appInstallResourceRepo.WithLinkId(app.ID),
|
|
appInstallResourceRepo.WithResourceIds(localResourceIDs),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appResources = append(appResources, resources...)
|
|
}
|
|
if len(remoteResourceIDs) != 0 {
|
|
resources, err := appInstallResourceRepo.GetBy(
|
|
appInstallResourceRepo.WithResourceIds(remoteResourceIDs),
|
|
appRepo.WithKey(dbType),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appResources = append(appResources, resources...)
|
|
}
|
|
|
|
appInstallIDs := make([]uint, 0, len(appResources))
|
|
appInstallIDSet := make(map[uint]struct{}, len(appResources))
|
|
for _, appResource := range appResources {
|
|
if appResource.AppInstallId == 0 {
|
|
continue
|
|
}
|
|
if _, ok := appInstallIDSet[appResource.AppInstallId]; ok {
|
|
continue
|
|
}
|
|
appInstallIDSet[appResource.AppInstallId] = struct{}{}
|
|
appInstallIDs = append(appInstallIDs, appResource.AppInstallId)
|
|
}
|
|
if len(appInstallIDs) == 0 {
|
|
return nil, nil
|
|
}
|
|
appInstalls, err := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(appInstallIDs))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
targets := make([]mysqlUserAppTarget, 0, len(appInstalls))
|
|
for _, appInstall := range appInstalls {
|
|
var envMap map[string]interface{}
|
|
if err := json.Unmarshal([]byte(appInstall.Env), &envMap); err != nil {
|
|
return nil, err
|
|
}
|
|
appUsername, ok := envMap["PANEL_DB_USER"].(string)
|
|
if !ok || appUsername != username {
|
|
continue
|
|
}
|
|
targets = append(targets, mysqlUserAppTarget{Key: appInstall.App.Key, Name: appInstall.Name})
|
|
}
|
|
return targets, nil
|
|
}
|
|
|
|
func checkMysqlUserAppUsage(dbType, database, username, host string, dbNames ...string) error {
|
|
targets, err := loadMysqlUserAppTargets(dbType, database, username, host, dbNames...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(targets) == 0 {
|
|
return nil
|
|
}
|
|
appNames := make([]string, 0, len(targets))
|
|
for _, target := range targets {
|
|
appNames = append(appNames, target.Name)
|
|
}
|
|
sort.Strings(appNames)
|
|
return buserr.WithDetail("ErrMysqlUserUsedByApps", strings.Join(appNames, ", "), nil)
|
|
}
|
|
|
|
func updateMysqlPasswordAppTargets(targets []mysqlUserAppTarget, password string) error {
|
|
for _, target := range targets {
|
|
global.LOG.Infof("start to update mysql password used by app %s-%s", target.Key, target.Name)
|
|
if err := updateInstallInfoInDB(target.Key, target.Name, "user-password", password); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func syncDatabaseUserMetadata(dbType, database string, users []client.UserInfo) error {
|
|
dbType = normalizeDatabaseUserType(dbType)
|
|
metas, err := databaseUserRepo.List(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
metaMap := make(map[string]model.DatabaseUser, len(metas))
|
|
for _, item := range metas {
|
|
metaMap[databaseUserKey(item.Username, item.Host)] = item
|
|
}
|
|
userMap := make(map[string]struct{}, len(users))
|
|
for _, item := range users {
|
|
if isMysqlSystemUser(item.Username) {
|
|
continue
|
|
}
|
|
key := databaseUserKey(item.Username, item.Host)
|
|
userMap[key] = struct{}{}
|
|
if meta, ok := metaMap[key]; ok {
|
|
if meta.IsDelete {
|
|
if err := databaseUserRepo.Update(map[string]interface{}{"is_delete": false, "password": ""}, repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database), databaseUserRepo.WithByUser(item.Username, item.Host)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if err := databaseUserRepo.Save(&model.DatabaseUser{
|
|
Type: dbType,
|
|
Database: database,
|
|
Username: item.Username,
|
|
Host: item.Host,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, item := range metas {
|
|
if isMysqlSystemUser(item.Username) {
|
|
continue
|
|
}
|
|
if _, ok := userMap[databaseUserKey(item.Username, item.Host)]; ok {
|
|
continue
|
|
}
|
|
if item.IsDelete {
|
|
continue
|
|
}
|
|
if err := databaseUserRepo.Update(map[string]interface{}{"is_delete": true, "password": ""}, repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database), databaseUserRepo.WithByUser(item.Username, item.Host)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func saveDatabaseUserGrant(dbType, database, dbName, username, host string) error {
|
|
dbType = normalizeDatabaseUserType(dbType)
|
|
grant, err := databaseUserGrantRepo.Get(
|
|
repo.WithByType(dbType),
|
|
databaseUserGrantRepo.WithByDatabase(database),
|
|
databaseUserGrantRepo.WithByDBName(dbName),
|
|
databaseUserGrantRepo.WithByUser(username, host),
|
|
)
|
|
if err == nil && grant.ID != 0 {
|
|
return nil
|
|
}
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
return databaseUserGrantRepo.Save(&model.DatabaseUserGrant{
|
|
Type: dbType,
|
|
Database: database,
|
|
DBName: dbName,
|
|
Username: username,
|
|
Host: host,
|
|
})
|
|
}
|
|
|
|
func syncDatabaseUserGrants(dbType, database string, grants []client.GrantInfo) error {
|
|
dbType = normalizeDatabaseUserType(dbType)
|
|
items := make([]model.DatabaseUserGrant, 0, len(grants))
|
|
grantMap := make(map[string]struct{}, len(grants))
|
|
for _, item := range grants {
|
|
if isMysqlSystemUser(item.Username) || item.Database == "*" {
|
|
continue
|
|
}
|
|
key := item.Database + "\x00" + item.Username + "\x00" + item.Host
|
|
if _, ok := grantMap[key]; ok {
|
|
continue
|
|
}
|
|
grantMap[key] = struct{}{}
|
|
items = append(items, model.DatabaseUserGrant{
|
|
Type: dbType,
|
|
Database: database,
|
|
DBName: item.Database,
|
|
Username: item.Username,
|
|
Host: item.Host,
|
|
})
|
|
}
|
|
return databaseUserGrantRepo.Replace(dbType, database, items)
|
|
}
|
|
|
|
func (u *MysqlService) SearchWithPage(search dto.MysqlDBSearch) (int64, interface{}, error) {
|
|
total, mysqls, err := mysqlRepo.Page(search.Page, search.PageSize,
|
|
mysqlRepo.WithByMysqlName(search.Database),
|
|
repo.WithByLikeName(search.Info),
|
|
repo.WithOrderRuleBy(search.OrderBy, search.Order),
|
|
)
|
|
var dtoMysqls []dto.MysqlDBInfo
|
|
for _, mysql := range mysqls {
|
|
var item dto.MysqlDBInfo
|
|
if err := copier.Copy(&item, &mysql); err != nil {
|
|
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
|
}
|
|
item.Username = ""
|
|
item.Password = ""
|
|
item.Permission = ""
|
|
dtoMysqls = append(dtoMysqls, item)
|
|
}
|
|
return total, dtoMysqls, err
|
|
}
|
|
|
|
func (u *MysqlService) ListDBOption() ([]dto.MysqlOption, error) {
|
|
mysqls, err := mysqlRepo.List()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
databases, err := databaseRepo.GetList(databaseRepo.WithTypeList("mysql,mariadb"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var dbs []dto.MysqlOption
|
|
for _, mysql := range mysqls {
|
|
var item dto.MysqlOption
|
|
if err := copier.Copy(&item, &mysql); err != nil {
|
|
return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
|
}
|
|
item.Database = mysql.MysqlName
|
|
for _, database := range databases {
|
|
if database.Name == item.Database {
|
|
item.Type = database.Type
|
|
}
|
|
}
|
|
dbs = append(dbs, item)
|
|
}
|
|
return dbs, err
|
|
}
|
|
|
|
func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) {
|
|
if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format, req.Collation, req.Permission) {
|
|
return nil, buserr.New("ErrCmdIllegal")
|
|
}
|
|
if len(req.Username) != 0 && len(req.Password) == 0 {
|
|
return nil, errors.New("password is required when creating mysql user")
|
|
}
|
|
permissionHosts := make([]string, 0)
|
|
if len(req.Username) != 0 {
|
|
var err error
|
|
permissionHosts, err = parseMysqlHosts(req.Permission, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Permission = strings.Join(permissionHosts, ",")
|
|
}
|
|
dbType, err := resolveDatabaseUserType(req.Database)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mysql, _ := mysqlRepo.Get(repo.WithByName(req.Name), mysqlRepo.WithByMysqlName(req.Database), repo.WithByFrom(req.From))
|
|
if mysql.ID != 0 {
|
|
return nil, buserr.New("ErrRecordExist")
|
|
}
|
|
|
|
var createItem model.DatabaseMysql
|
|
if err := copier.Copy(&createItem, &req); err != nil {
|
|
return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
|
}
|
|
createItem.Username = ""
|
|
createItem.Password = ""
|
|
createItem.Permission = ""
|
|
|
|
if req.From == "local" && req.Username == "root" {
|
|
return nil, errors.New("cannot set root as user name")
|
|
}
|
|
|
|
cli, version, err := LoadMysqlClientByFrom(req.Database)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
createItem.MysqlName = req.Database
|
|
defer cli.Close()
|
|
if err := cli.Create(client.CreateInfo{
|
|
Name: req.Name,
|
|
Format: req.Format,
|
|
Collation: req.Collation,
|
|
Username: req.Username,
|
|
Password: req.Password,
|
|
Permission: req.Permission,
|
|
Version: version,
|
|
Timeout: 300,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(req.Username) != 0 {
|
|
if err := saveDatabaseUserCredentials(dbType, req.Database, req.Username, req.Permission, req.Password, req.Description); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, host := range permissionHosts {
|
|
if err := saveDatabaseUserGrant(dbType, req.Database, req.Name, req.Username, host); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
global.LOG.Infof("create database %s successful!", req.Name)
|
|
if err := mysqlRepo.Create(ctx, &createItem); err != nil {
|
|
return nil, err
|
|
}
|
|
return &createItem, nil
|
|
}
|
|
|
|
func (u *MysqlService) ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, error) {
|
|
dbType, err := resolveDatabaseUserType(req.Database)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
users, err := databaseUserRepo.List(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res := make([]dto.MysqlUser, 0, len(users))
|
|
for _, user := range users {
|
|
if isMysqlSystemUser(user.Username) {
|
|
continue
|
|
}
|
|
res = append(res, dto.MysqlUser{
|
|
Username: user.Username,
|
|
Host: user.Host,
|
|
Password: user.Password,
|
|
Description: user.Description,
|
|
IsDelete: user.IsDelete,
|
|
})
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (u *MysqlService) ListGrants(req dto.MysqlUserSearch) ([]dto.MysqlGrant, error) {
|
|
dbType, err := resolveDatabaseUserType(req.Database)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
grants, err := databaseUserGrantRepo.List(repo.WithByType(dbType), databaseUserGrantRepo.WithByDatabase(req.Database))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res := make([]dto.MysqlGrant, 0, len(grants))
|
|
for _, grant := range grants {
|
|
res = append(res, dto.MysqlGrant{Database: grant.DBName, Username: grant.Username, Host: grant.Host})
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (u *MysqlService) ListGrantSummary(req dto.MysqlGrantSummarySearch) (map[string][]dto.MysqlUser, error) {
|
|
res := make(map[string][]dto.MysqlUser, len(req.DBs))
|
|
dbMap := make(map[string]struct{}, len(req.DBs))
|
|
dbNames := make([]string, 0, len(req.DBs))
|
|
for _, item := range req.DBs {
|
|
if item == "" {
|
|
continue
|
|
}
|
|
if _, ok := dbMap[item]; ok {
|
|
continue
|
|
}
|
|
dbMap[item] = struct{}{}
|
|
dbNames = append(dbNames, item)
|
|
res[item] = []dto.MysqlUser{}
|
|
}
|
|
if len(dbMap) == 0 {
|
|
return res, nil
|
|
}
|
|
dbType, err := resolveDatabaseUserType(req.Database)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
grants, err := databaseUserGrantRepo.List(
|
|
repo.WithByType(dbType),
|
|
databaseUserGrantRepo.WithByDatabase(req.Database),
|
|
databaseUserGrantRepo.WithByDBNames(dbNames),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
userList := make([][2]string, 0, len(grants))
|
|
userSet := make(map[string]struct{}, len(grants))
|
|
for _, grant := range grants {
|
|
key := databaseUserKey(grant.Username, grant.Host)
|
|
if _, ok := userSet[key]; ok {
|
|
continue
|
|
}
|
|
userSet[key] = struct{}{}
|
|
userList = append(userList, [2]string{grant.Username, grant.Host})
|
|
}
|
|
userMetas, err := databaseUserRepo.List(
|
|
repo.WithByType(dbType),
|
|
databaseUserRepo.WithByDatabase(req.Database),
|
|
databaseUserRepo.WithByUserList(userList),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
metaMap := make(map[string]model.DatabaseUser, len(userMetas))
|
|
for _, item := range userMetas {
|
|
metaMap[databaseUserKey(item.Username, item.Host)] = item
|
|
}
|
|
|
|
for _, grant := range grants {
|
|
if _, ok := dbMap[grant.DBName]; !ok {
|
|
continue
|
|
}
|
|
meta, ok := metaMap[databaseUserKey(grant.Username, grant.Host)]
|
|
if !ok || meta.IsDelete {
|
|
continue
|
|
}
|
|
item := dto.MysqlUser{Username: grant.Username, Host: grant.Host, Description: meta.Description}
|
|
res[grant.DBName] = append(res[grant.DBName], item)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (u *MysqlService) CreateUser(req dto.MysqlUserCreate) error {
|
|
if cmd.CheckIllegal(req.Username, req.Password, req.Host) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
dbs := make([]string, 0, len(req.DBs))
|
|
dbSet := make(map[string]struct{}, len(req.DBs))
|
|
for _, db := range req.DBs {
|
|
if db == "" {
|
|
continue
|
|
}
|
|
if cmd.CheckIllegal(db) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
if db == "*" {
|
|
return errors.New("global mysql privileges must be managed outside 1Panel")
|
|
}
|
|
if _, ok := dbSet[db]; ok {
|
|
continue
|
|
}
|
|
dbSet[db] = struct{}{}
|
|
dbs = append(dbs, db)
|
|
}
|
|
if err := checkMysqlNormalUser(req.Username); err != nil {
|
|
return err
|
|
}
|
|
hosts, err := parseMysqlHosts(req.Host, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dbType, err := resolveDatabaseUserType(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cli, version, err := LoadMysqlClientByFrom(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
createdHosts := make([]string, 0, len(hosts))
|
|
rollbackCreatedHosts := func() {
|
|
for i := len(createdHosts) - 1; i >= 0; i-- {
|
|
host := createdHosts[i]
|
|
if rollbackErr := cli.DeleteUser(client.UserInfo{Username: req.Username, Host: host}, version, 300); rollbackErr != nil {
|
|
global.LOG.Errorf("rollback mysql user %s@%s failed, err: %v", req.Username, host, rollbackErr)
|
|
}
|
|
}
|
|
}
|
|
savedHosts := make([]string, 0, len(hosts))
|
|
rollbackSavedHosts := func() {
|
|
for i := len(savedHosts) - 1; i >= 0; i-- {
|
|
host := savedHosts[i]
|
|
if rollbackErr := databaseUserRepo.Delete(
|
|
repo.WithByType(dbType),
|
|
databaseUserRepo.WithByDatabase(req.Database),
|
|
databaseUserRepo.WithByUser(req.Username, host),
|
|
); rollbackErr != nil {
|
|
global.LOG.Errorf("rollback mysql user record %s@%s failed, err: %v", req.Username, host, rollbackErr)
|
|
}
|
|
}
|
|
}
|
|
grantedItems := make([]client.GrantInfo, 0, len(dbs)*len(hosts))
|
|
rollbackGrantedItems := func() {
|
|
for i := len(grantedItems) - 1; i >= 0; i-- {
|
|
item := grantedItems[i]
|
|
if rollbackErr := cli.RevokeGrant(item, 300); rollbackErr != nil {
|
|
global.LOG.Errorf(
|
|
"rollback mysql grant %s to %s@%s failed, err: %v",
|
|
item.Database,
|
|
item.Username,
|
|
item.Host,
|
|
rollbackErr,
|
|
)
|
|
}
|
|
if rollbackErr := databaseUserGrantRepo.Delete(
|
|
repo.WithByType(dbType),
|
|
databaseUserGrantRepo.WithByDatabase(req.Database),
|
|
databaseUserGrantRepo.WithByDBName(item.Database),
|
|
databaseUserGrantRepo.WithByUser(item.Username, item.Host),
|
|
); rollbackErr != nil {
|
|
global.LOG.Errorf(
|
|
"rollback mysql grant record %s to %s@%s failed, err: %v",
|
|
item.Database,
|
|
item.Username,
|
|
item.Host,
|
|
rollbackErr,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
rollbackAll := func() {
|
|
rollbackGrantedItems()
|
|
rollbackSavedHosts()
|
|
rollbackCreatedHosts()
|
|
}
|
|
for _, host := range hosts {
|
|
if err := cli.CreateUserOnly(client.UserInfo{Username: req.Username, Host: host}, req.Password, 300); err != nil {
|
|
rollbackCreatedHosts()
|
|
return err
|
|
}
|
|
createdHosts = append(createdHosts, host)
|
|
}
|
|
for _, host := range hosts {
|
|
if err := saveDatabaseUserCredential(dbType, req.Database, req.Username, host, req.Password, req.Description); err != nil {
|
|
rollbackAll()
|
|
return err
|
|
}
|
|
savedHosts = append(savedHosts, host)
|
|
}
|
|
for _, db := range dbs {
|
|
for _, host := range hosts {
|
|
item := client.GrantInfo{Database: db, Username: req.Username, Host: host}
|
|
if err := cli.GrantUser(item, 300); err != nil {
|
|
rollbackAll()
|
|
return err
|
|
}
|
|
grantedItems = append(grantedItems, item)
|
|
if err := saveDatabaseUserGrant(dbType, req.Database, db, req.Username, host); err != nil {
|
|
rollbackAll()
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *MysqlService) UpdateUser(req dto.MysqlUserUpdate) error {
|
|
if cmd.CheckIllegal(req.Username, req.Host, req.NewHost) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
if err := checkMysqlNormalUser(req.Username); err != nil {
|
|
return err
|
|
}
|
|
oldHosts, err := parseMysqlHosts(req.Host, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newHosts, err := parseMysqlHosts(req.NewHost, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dbType, err := resolveDatabaseUserType(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Host = oldHosts[0]
|
|
req.NewHost = newHosts[0]
|
|
if req.Host != req.NewHost {
|
|
targetUser, _ := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.NewHost))
|
|
if targetUser.ID != 0 {
|
|
return buserr.New("ErrRecordExist")
|
|
}
|
|
if err := checkMysqlUserAppUsage(dbType, req.Database, req.Username, req.Host); err != nil {
|
|
return err
|
|
}
|
|
cli, _, err := LoadMysqlClientByFrom(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
if err := cli.UpdateUser(client.UserUpdateInfo{Username: req.Username, Host: req.Host, NewHost: req.NewHost}, 300); err != nil {
|
|
return err
|
|
}
|
|
user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
|
|
if err != nil {
|
|
user = model.DatabaseUser{
|
|
Type: dbType,
|
|
Database: req.Database,
|
|
Username: req.Username,
|
|
}
|
|
}
|
|
user.Host = req.NewHost
|
|
user.IsDelete = false
|
|
user.Description = req.Description
|
|
if err := databaseUserRepo.Save(&user); err != nil {
|
|
if rollbackErr := cli.UpdateUser(client.UserUpdateInfo{Username: req.Username, Host: req.NewHost, NewHost: req.Host}, 300); rollbackErr != nil {
|
|
global.LOG.Errorf("rollback mysql user %s host from %s to %s failed, err: %v", req.Username, req.NewHost, req.Host, rollbackErr)
|
|
}
|
|
return err
|
|
}
|
|
if err := databaseUserGrantRepo.Update(
|
|
map[string]interface{}{"host": req.NewHost},
|
|
repo.WithByType(dbType),
|
|
databaseUserGrantRepo.WithByDatabase(req.Database),
|
|
databaseUserGrantRepo.WithByUser(req.Username, req.Host),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
|
|
if err != nil {
|
|
user = model.DatabaseUser{
|
|
Type: dbType,
|
|
Database: req.Database,
|
|
Username: req.Username,
|
|
Host: req.Host,
|
|
}
|
|
}
|
|
user.IsDelete = false
|
|
user.Description = req.Description
|
|
return databaseUserRepo.Save(&user)
|
|
}
|
|
|
|
func (u *MysqlService) ChangeUserPassword(req dto.MysqlUserPassword) error {
|
|
if cmd.CheckIllegal(req.Username, req.Host, req.Password) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
if err := checkMysqlNormalUser(req.Username); err != nil {
|
|
return err
|
|
}
|
|
hosts, err := parseMysqlHosts(req.Host, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Host = hosts[0]
|
|
dbType, err := resolveDatabaseUserType(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cli, version, err := LoadMysqlClientByFrom(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
grants, err := cli.ListGrants(300)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
grantDBs := make([]string, 0)
|
|
for _, grant := range grants {
|
|
if grant.Username == req.Username && grant.Host == req.Host {
|
|
grantDBs = append(grantDBs, grant.Database)
|
|
}
|
|
}
|
|
var appTargets []mysqlUserAppTarget
|
|
if len(grantDBs) != 0 {
|
|
appTargets, err = loadMysqlUserAppTargets(dbType, req.Database, req.Username, req.Host, grantDBs...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := cli.ChangePassword(client.PasswordChangeInfo{
|
|
Username: req.Username,
|
|
Permission: req.Host,
|
|
Password: req.Password,
|
|
Version: version,
|
|
Timeout: 300,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := saveDatabaseUserCredential(dbType, req.Database, req.Username, req.Host, req.Password, ""); err != nil {
|
|
return err
|
|
}
|
|
return updateMysqlPasswordAppTargets(appTargets, req.Password)
|
|
}
|
|
|
|
func (u *MysqlService) SaveUserPassword(req dto.MysqlUserPassword) error {
|
|
if cmd.CheckIllegal(req.Username, req.Host, req.Password) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
if err := checkMysqlNormalUser(req.Username); err != nil {
|
|
return err
|
|
}
|
|
hosts, err := parseMysqlHosts(req.Host, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Host = hosts[0]
|
|
dbType, err := resolveDatabaseUserType(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if user.IsDelete {
|
|
return errors.New("cannot save password for a deleted mysql user")
|
|
}
|
|
user.Password = req.Password
|
|
return databaseUserRepo.Save(&user)
|
|
}
|
|
|
|
func (u *MysqlService) DeleteUser(req dto.MysqlUserDelete) error {
|
|
if cmd.CheckIllegal(req.Username, req.Host) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
if err := checkMysqlNormalUser(req.Username); err != nil {
|
|
return err
|
|
}
|
|
hosts, err := parseMysqlHosts(req.Host, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Host = hosts[0]
|
|
dbType, err := resolveDatabaseUserType(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := checkMysqlUserAppUsage(dbType, req.Database, req.Username, req.Host); err != nil {
|
|
return err
|
|
}
|
|
user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
if err != nil || !user.IsDelete {
|
|
cli, version, err := LoadMysqlClientByFrom(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
if err := cli.DeleteUser(client.UserInfo{Username: req.Username, Host: req.Host}, version, 300); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := databaseUserGrantRepo.Delete(repo.WithByType(dbType), databaseUserGrantRepo.WithByDatabase(req.Database), databaseUserGrantRepo.WithByUser(req.Username, req.Host)); err != nil {
|
|
return err
|
|
}
|
|
return databaseUserRepo.Delete(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
|
|
}
|
|
|
|
func (u *MysqlService) GrantUser(req dto.MysqlGrantCreate) error {
|
|
if cmd.CheckIllegal(req.DB, req.Username, req.Host) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
if req.DB == "*" {
|
|
return errors.New("global mysql privileges must be managed outside 1Panel")
|
|
}
|
|
if err := checkMysqlNormalUser(req.Username); err != nil {
|
|
return err
|
|
}
|
|
hosts, err := parseMysqlHosts(req.Host, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dbType, err := resolveDatabaseUserType(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cli, _, err := LoadMysqlClientByFrom(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
for _, host := range hosts {
|
|
if err := cli.GrantUser(client.GrantInfo{Database: req.DB, Username: req.Username, Host: host}, 300); err != nil {
|
|
return err
|
|
}
|
|
if err := saveDatabaseUserGrant(dbType, req.Database, req.DB, req.Username, host); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *MysqlService) RevokeGrant(req dto.MysqlGrantDelete) error {
|
|
if cmd.CheckIllegal(req.DB, req.Username, req.Host) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
if req.DB == "*" {
|
|
return errors.New("global mysql privileges must be managed outside 1Panel")
|
|
}
|
|
if err := checkMysqlNormalUser(req.Username); err != nil {
|
|
return err
|
|
}
|
|
hosts, err := parseMysqlHosts(req.Host, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Host = hosts[0]
|
|
dbType, err := resolveDatabaseUserType(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := checkMysqlUserAppUsage(dbType, req.Database, req.Username, req.Host, req.DB); err != nil {
|
|
return err
|
|
}
|
|
cli, _, err := LoadMysqlClientByFrom(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
if err := cli.RevokeGrant(client.GrantInfo{Database: req.DB, Username: req.Username, Host: req.Host}, 300); err != nil {
|
|
return err
|
|
}
|
|
return databaseUserGrantRepo.Delete(
|
|
repo.WithByType(dbType),
|
|
databaseUserGrantRepo.WithByDatabase(req.Database),
|
|
databaseUserGrantRepo.WithByDBName(req.DB),
|
|
databaseUserGrantRepo.WithByUser(req.Username, req.Host),
|
|
)
|
|
}
|
|
|
|
func (u *MysqlService) LoadFromRemote(req dto.MysqlLoadDB) error {
|
|
dbType, err := resolveDatabaseUserType(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
client, version, err := LoadMysqlClientByFrom(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
|
|
databases, err := mysqlRepo.List(mysqlRepo.WithByMysqlName(req.Database))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
datas, err := client.SyncDB(version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
users, err := client.ListUsers(300)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
grants, err := client.ListGrants(300)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := syncDatabaseUserMetadata(dbType, req.Database, users); err != nil {
|
|
return err
|
|
}
|
|
if err := syncDatabaseUserGrants(dbType, req.Database, grants); err != nil {
|
|
return err
|
|
}
|
|
deleteList := databases
|
|
for _, data := range datas {
|
|
hasOld := false
|
|
for i := 0; i < len(databases); i++ {
|
|
if strings.EqualFold(databases[i].Name, data.Name) && strings.EqualFold(databases[i].MysqlName, data.MysqlName) {
|
|
hasOld = true
|
|
if databases[i].IsDelete {
|
|
_ = mysqlRepo.Update(databases[i].ID, map[string]interface{}{"is_delete": false})
|
|
}
|
|
deleteList = append(deleteList[:i], deleteList[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
if !hasOld {
|
|
var createItem model.DatabaseMysql
|
|
if err := copier.Copy(&createItem, &data); err != nil {
|
|
return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
|
}
|
|
createItem.Username = ""
|
|
createItem.Password = ""
|
|
createItem.Permission = ""
|
|
if err := mysqlRepo.Create(context.Background(), &createItem); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
for _, delItem := range deleteList {
|
|
_ = mysqlRepo.Update(delItem.ID, map[string]interface{}{"is_delete": true})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *MysqlService) UpdateDescription(req dto.UpdateDescription) error {
|
|
return mysqlRepo.Update(req.ID, map[string]interface{}{"description": req.Description})
|
|
}
|
|
|
|
func loadMysqlDeleteTarget(id uint) (model.DatabaseMysql, string, error) {
|
|
db, err := mysqlRepo.Get(repo.WithByID(id))
|
|
if err != nil {
|
|
return db, "", err
|
|
}
|
|
dbType, err := resolveDatabaseUserType(db.MysqlName)
|
|
if err != nil {
|
|
return db, "", err
|
|
}
|
|
return db, dbType, nil
|
|
}
|
|
|
|
func (u *MysqlService) deleteCheck(db model.DatabaseMysql, dbType string) ([]dto.DBResource, error) {
|
|
var res []dto.DBResource
|
|
websites, err := websiteRepo.GetBy(
|
|
websiteRepo.WithDBTypes([]string{constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster}),
|
|
websiteRepo.WithDBID(db.ID),
|
|
)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
for _, website := range websites {
|
|
res = append(res, dto.DBResource{
|
|
Type: constant.TypeWebsite,
|
|
Name: website.PrimaryDomain,
|
|
})
|
|
}
|
|
|
|
if db.From == "local" {
|
|
app, err := appInstallRepo.LoadBaseInfo(dbType, db.MysqlName)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
apps, err := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(db.ID))
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
for _, app := range apps {
|
|
appInstall, err := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId))
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
continue
|
|
}
|
|
return res, err
|
|
}
|
|
if appInstall.ID != 0 {
|
|
res = append(res, dto.DBResource{
|
|
Type: constant.TypeApp,
|
|
Name: appInstall.Name,
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
apps, err := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(db.ID), appRepo.WithKey(dbType))
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
for _, app := range apps {
|
|
appInstall, err := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId))
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
continue
|
|
}
|
|
return res, err
|
|
}
|
|
if appInstall.ID != 0 {
|
|
res = append(res, dto.DBResource{
|
|
Type: constant.TypeApp,
|
|
Name: appInstall.Name,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (u *MysqlService) DeleteCheck(req dto.MysqlDBDeleteCheck) ([]dto.DBResource, error) {
|
|
db, dbType, err := loadMysqlDeleteTarget(req.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return u.deleteCheck(db, dbType)
|
|
}
|
|
|
|
func (u *MysqlService) Delete(ctx context.Context, req dto.MysqlDBDelete) error {
|
|
return u.delete(ctx, req, nil)
|
|
}
|
|
|
|
func (u *MysqlService) delete(ctx context.Context, req dto.MysqlDBDelete, exclusions []dto.DBResource) error {
|
|
db, dbType, err := loadMysqlDeleteTarget(req.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resources, err := u.deleteCheck(db, dbType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(exclusions) != 0 {
|
|
exclusionMap := make(map[string]struct{}, len(exclusions))
|
|
for _, exclusion := range exclusions {
|
|
exclusionMap[exclusion.Type+"\x00"+exclusion.Name] = struct{}{}
|
|
}
|
|
filtered := resources[:0]
|
|
for _, resource := range resources {
|
|
if _, ok := exclusionMap[resource.Type+"\x00"+resource.Name]; ok {
|
|
continue
|
|
}
|
|
filtered = append(filtered, resource)
|
|
}
|
|
resources = filtered
|
|
}
|
|
if len(resources) != 0 {
|
|
names := make([]string, 0, len(resources))
|
|
for _, resource := range resources {
|
|
names = append(names, resource.Name)
|
|
}
|
|
sort.Strings(names)
|
|
return buserr.WithDetail("ErrInUsed", strings.Join(names, ", "), nil)
|
|
}
|
|
cli, version, err := LoadMysqlClientByFrom(db.MysqlName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
if err := cli.DeleteDatabase(client.DeleteInfo{
|
|
Name: db.Name,
|
|
Version: version,
|
|
Timeout: 300,
|
|
}); err != nil && !req.ForceDelete {
|
|
return err
|
|
}
|
|
|
|
if req.DeleteBackup {
|
|
uploadDir := filepath.Join(global.Dir.DataDir, fmt.Sprintf("uploads/database/%s/%s/%s", dbType, db.MysqlName, db.Name))
|
|
if _, err := os.Stat(uploadDir); err == nil {
|
|
_ = os.RemoveAll(uploadDir)
|
|
}
|
|
backupDir := filepath.Join(global.Dir.LocalBackupDir, fmt.Sprintf("database/%s/%s/%s", dbType, db.MysqlName, db.Name))
|
|
if _, err := os.Stat(backupDir); err == nil {
|
|
_ = os.RemoveAll(backupDir)
|
|
}
|
|
_ = backupRepo.DeleteRecord(ctx, repo.WithByType(dbType), repo.WithByName(db.MysqlName), repo.WithByDetailName(db.Name))
|
|
global.LOG.Infof("delete database %s-%s backups successful", db.MysqlName, db.Name)
|
|
}
|
|
|
|
_ = mysqlRepo.Delete(ctx, repo.WithByID(db.ID))
|
|
return nil
|
|
}
|
|
|
|
func deleteMysqlDatabaseForResourceOwner(ctx context.Context, req dto.MysqlDBDelete, owner dto.DBResource) error {
|
|
return (&MysqlService{}).delete(ctx, req, []dto.DBResource{owner})
|
|
}
|
|
|
|
func isMysqlDatabaseResourceInUseError(err error) bool {
|
|
businessErr, ok := err.(buserr.BusinessError)
|
|
return ok && businessErr.Msg == "ErrInUsed"
|
|
}
|
|
|
|
func (u *MysqlService) ChangePassword(req dto.ChangeDBInfo) error {
|
|
if cmd.CheckIllegal(req.Value) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
cli, version, err := LoadMysqlClientByFrom(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
if req.ID != 0 {
|
|
return errors.New("mysql user password should be changed by user api")
|
|
}
|
|
passwordInfo := client.PasswordChangeInfo{
|
|
Username: "root",
|
|
Password: req.Value,
|
|
Timeout: 300,
|
|
Version: version,
|
|
}
|
|
if err := cli.ChangePassword(passwordInfo); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := updateInstallInfoInDB(req.Type, req.Database, "password", req.Value); err != nil {
|
|
return err
|
|
}
|
|
if req.From == "local" {
|
|
remote, err := databaseRepo.Get(repo.WithByName(req.Database))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pass, err := encrypt.StringEncrypt(req.Value)
|
|
if err != nil {
|
|
return fmt.Errorf("decrypt database password failed, err: %v", err)
|
|
}
|
|
_ = databaseRepo.Update(remote.ID, map[string]interface{}{"password": pass})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *MysqlService) ChangeAccess(req dto.ChangeDBInfo) error {
|
|
if cmd.CheckIllegal(req.Value) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
cli, version, err := LoadMysqlClientByFrom(req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
if req.ID != 0 {
|
|
return errors.New("mysql user access should be changed by user api")
|
|
}
|
|
accessInfo := client.AccessChangeInfo{
|
|
Username: "root",
|
|
Permission: req.Value,
|
|
Timeout: 300,
|
|
Version: version,
|
|
}
|
|
if err := cli.ChangeAccess(accessInfo); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *MysqlService) UpdateVariables(req dto.MysqlVariablesUpdate) error {
|
|
app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var files []string
|
|
|
|
path := fmt.Sprintf("%s/%s/%s/conf/my.cnf", global.Dir.AppInstallDir, req.Type, app.Name)
|
|
lineBytes, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
files = strings.Split(string(lineBytes), "\n")
|
|
|
|
group := "[mysqld]"
|
|
for _, info := range req.Variables {
|
|
if info.Param == "slow_query_log" && info.Value == "ON" {
|
|
logFilePath := filepath.Join(global.Dir.DataDir, fmt.Sprintf("apps/%s/%s/data/1Panel-slow.log", app.Key, app.Name))
|
|
if req.Type == "mariadb" {
|
|
logFilePath = filepath.Join(global.Dir.DataDir, fmt.Sprintf("apps/%s/%s/db/data/1Panel-slow.log", app.Key, app.Name))
|
|
}
|
|
file, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
}
|
|
if !strings.HasPrefix(app.Version, "5.7") && !strings.HasPrefix(app.Version, "5.6") {
|
|
if info.Param == "query_cache_size" {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if _, ok := info.Value.(float64); ok {
|
|
files = updateMyCnf(files, group, info.Param, common.LoadSizeUnit(info.Value.(float64)))
|
|
} else {
|
|
files = updateMyCnf(files, group, info.Param, info.Value)
|
|
}
|
|
}
|
|
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, constant.FilePerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
_, err = file.WriteString(strings.Join(files, "\n"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", global.Dir.AppInstallDir, req.Type, app.Name)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *MysqlService) LoadRemoteAccess(req dto.OperationWithNameAndType) (bool, error) {
|
|
app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
hosts, err := executeSqlForRows(app.ContainerName, app.Key, app.Password, "select host from mysql.user where user='root';")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, host := range hosts {
|
|
if host == "%" {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (u *MysqlService) LoadVariables(req dto.OperationWithNameAndType) (*dto.MysqlVariables, error) {
|
|
app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
variableMap, err := executeSqlForMaps(app.ContainerName, app.Key, app.Password, "show global variables;")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var info dto.MysqlVariables
|
|
arr, err := json.Marshal(variableMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_ = json.Unmarshal(arr, &info)
|
|
return &info, nil
|
|
}
|
|
|
|
func (u *MysqlService) LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlStatus, error) {
|
|
app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
statusMap, err := executeSqlForMaps(app.ContainerName, app.Key, app.Password, "show global status;")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var info dto.MysqlStatus
|
|
arr, err := json.Marshal(statusMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_ = json.Unmarshal(arr, &info)
|
|
|
|
if value, ok := statusMap["Run"]; ok {
|
|
uptime, _ := strconv.Atoi(value)
|
|
info.Run = time.Unix(time.Now().Unix()-int64(uptime), 0).Format(constant.DateTimeLayout)
|
|
} else {
|
|
if value, ok := statusMap["Uptime"]; ok {
|
|
uptime, _ := strconv.Atoi(value)
|
|
info.Run = time.Unix(time.Now().Unix()-int64(uptime), 0).Format(constant.DateTimeLayout)
|
|
}
|
|
}
|
|
|
|
info.File = "OFF"
|
|
info.Position = "OFF"
|
|
masterStatus := "show master status;"
|
|
if common.CompareAppVersion(app.Version, "8.4.0") && (req.Type == constant.AppMysql || req.Type == constant.AppMysqlCluster) {
|
|
masterStatus = "show binary log status;"
|
|
}
|
|
rows, err := executeSqlForRows(app.ContainerName, app.Key, app.Password, masterStatus)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(rows) > 2 {
|
|
itemValue := strings.Split(rows[1], "\t")
|
|
if len(itemValue) > 2 {
|
|
info.File = itemValue[0]
|
|
info.Position = itemValue[1]
|
|
}
|
|
}
|
|
|
|
return &info, nil
|
|
}
|
|
|
|
func (u *MysqlService) LoadFormatOption(req dto.OperationWithName) []dto.MysqlFormatCollationOption {
|
|
defaultList := []dto.MysqlFormatCollationOption{{Format: "utf8mb4"}, {Format: "utf8mb3"}, {Format: "gbk"}, {Format: "big5"}}
|
|
client, _, err := LoadMysqlClientByFrom(req.Name)
|
|
if err != nil {
|
|
return defaultList
|
|
}
|
|
options, err := client.LoadFormatCollation(3)
|
|
if err != nil {
|
|
return defaultList
|
|
}
|
|
return options
|
|
}
|
|
|
|
func executeSqlForMaps(containerName, dbType, password, command string) (map[string]string, error) {
|
|
if dbType == "mysql-cluster" {
|
|
dbType = "mysql"
|
|
}
|
|
cmd := exec.Command("docker", "exec", containerName, dbType, "-uroot", "-p"+password, "-e", command)
|
|
stdout, err := cmd.CombinedOutput()
|
|
stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
|
|
if err != nil || strings.HasPrefix(stdStr, "ERROR ") {
|
|
return nil, errors.New(stdStr)
|
|
}
|
|
|
|
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 executeSqlForRows(containerName, dbType, password, command string) ([]string, error) {
|
|
if dbType == "mysql-cluster" {
|
|
dbType = "mysql"
|
|
}
|
|
cmd := exec.Command("docker", "exec", containerName, dbType, "-uroot", "-p"+password, "-e", command)
|
|
stdout, err := cmd.CombinedOutput()
|
|
stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
|
|
if err != nil || strings.HasPrefix(stdStr, "ERROR ") {
|
|
return nil, errors.New(stdStr)
|
|
}
|
|
return strings.Split(stdStr, "\n"), nil
|
|
}
|
|
|
|
func updateMyCnf(oldFiles []string, group string, param string, value interface{}) []string {
|
|
isOn := false
|
|
hasGroup := false
|
|
hasKey := false
|
|
regItem := re.GetRegex(re.MysqlGroupPattern)
|
|
var newFiles []string
|
|
i := 0
|
|
for _, line := range oldFiles {
|
|
i++
|
|
if strings.HasPrefix(line, group) {
|
|
isOn = true
|
|
hasGroup = true
|
|
newFiles = append(newFiles, line)
|
|
continue
|
|
}
|
|
if !isOn {
|
|
newFiles = append(newFiles, line)
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, param+"=") || strings.HasPrefix(line, "# "+param+"=") {
|
|
newFiles = append(newFiles, fmt.Sprintf("%s=%v", param, value))
|
|
hasKey = true
|
|
continue
|
|
}
|
|
if regItem.Match([]byte(line)) || i == len(oldFiles) {
|
|
isOn = false
|
|
if !hasKey {
|
|
newFiles = append(newFiles, fmt.Sprintf("%s=%v", param, value))
|
|
}
|
|
newFiles = append(newFiles, line)
|
|
continue
|
|
}
|
|
newFiles = append(newFiles, line)
|
|
}
|
|
if !hasGroup {
|
|
newFiles = append(newFiles, group+"\n")
|
|
newFiles = append(newFiles, fmt.Sprintf("%s=%v\n", param, value))
|
|
}
|
|
return newFiles
|
|
}
|
|
|
|
func LoadMysqlClientByFrom(database string) (mysql.MysqlClient, string, error) {
|
|
var (
|
|
dbInfo client.DBInfo
|
|
version string
|
|
err error
|
|
)
|
|
|
|
dbInfo.Timeout = 300
|
|
databaseItem, err := databaseRepo.Get(repo.WithByName(database))
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
dbInfo.Type = databaseItem.Type
|
|
dbInfo.From = databaseItem.From
|
|
dbInfo.Database = database
|
|
if dbInfo.From != "local" {
|
|
dbInfo.Address = databaseItem.Address
|
|
dbInfo.Port = databaseItem.Port
|
|
dbInfo.Username = databaseItem.Username
|
|
dbInfo.Password = databaseItem.Password
|
|
dbInfo.SSL = databaseItem.SSL
|
|
dbInfo.ClientKey = databaseItem.ClientKey
|
|
dbInfo.ClientCert = databaseItem.ClientCert
|
|
dbInfo.RootCert = databaseItem.RootCert
|
|
dbInfo.SkipVerify = databaseItem.SkipVerify
|
|
version = databaseItem.Version
|
|
|
|
} else {
|
|
app, err := appInstallRepo.LoadBaseInfo(databaseItem.Type, database)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
dbInfo.Address = app.ContainerName
|
|
dbInfo.Username = "root"
|
|
dbInfo.Password = app.Password
|
|
version = app.Version
|
|
}
|
|
|
|
cli, err := mysql.NewMysqlClient(dbInfo)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return cli, version, nil
|
|
}
|