Files
1Panel/agent/app/service/database_redis.go
T
Yuki Guoandssongliu 486cd5d0ad feat(redis): enable remote terminal through tracked background tasks (#14012)
* feat(redis): enable remote terminal through tracked background tasks

* feat(redis): enable remote terminal through tracked background tasks

* refactor(redis): remove redundant CLI status tracking

* refactor(redis): retain existing task execution flow

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-09 18:20:51 +08:00

346 lines
10 KiB
Go

package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/google/uuid"
"gorm.io/gorm"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
dockerclient "github.com/docker/docker/client"
_ "github.com/go-sql-driver/mysql"
)
type RedisService struct{}
// The CLI container is shared by all remote Redis databases on this node.
var redisCliInstallMutex sync.Mutex
type IRedisService interface {
UpdateConf(req dto.RedisConfUpdate) error
UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error
ChangePassword(info dto.ChangeRedisPass) error
LoadStatus(req dto.LoadRedisStatus) (*dto.RedisStatus, error)
LoadConf(req dto.LoadRedisStatus) (*dto.RedisConf, error)
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
CheckHasCli() bool
InstallCli(req dto.RedisCliInstall) (string, error)
}
func NewIRedisService() IRedisService {
return &RedisService{}
}
func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error {
redisInfo, err := appInstallRepo.LoadBaseInfo(req.DBType, req.Database)
if err != nil {
return err
}
var confs []redisConfig
confs = append(confs, redisConfig{key: "timeout", value: req.Timeout})
confs = append(confs, redisConfig{key: "maxclients", value: req.Maxclients})
confs = append(confs, redisConfig{key: "maxmemory", value: req.Maxmemory})
if err := confSet(redisInfo.Name, req.DBType, "", confs); err != nil {
return err
}
if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", global.Dir.AppInstallDir, req.DBType, redisInfo.Name)); err != nil {
return err
}
return nil
}
func (u *RedisService) CheckHasCli() bool {
installed, _ := u.checkCliInstalled()
return installed
}
func (u *RedisService) checkCliInstalled() (bool, error) {
client, err := docker.NewDockerClient()
if err != nil {
return false, err
}
defer client.Close()
info, err := client.ContainerInspect(context.Background(), "1Panel-redis-cli-tools")
if dockerclient.IsErrNotFound(err) {
return false, nil
}
if err != nil {
return false, err
}
return info.ContainerJSONBase != nil && info.State != nil && info.State.Running, nil
}
func (u *RedisService) InstallCli(req dto.RedisCliInstall) (string, error) {
if !redisCliInstallMutex.TryLock() {
return "", buserr.New("TaskIsExecuting")
}
defer redisCliInstallMutex.Unlock()
item := dto.ContainerOperate{
TaskID: req.TaskID,
Name: "1Panel-redis-cli-tools",
Image: "redis:7.4.4",
Networks: []dto.ContainerNetwork{{Network: "1panel-network"}},
}
running, err := taskRepo.GetFirst(
repo.WithByName(task.GetTaskName(item.Name, task.TaskCreate, task.TaskScopeContainer)),
repo.WithByType(task.TaskScopeContainer),
taskRepo.WithByStatus(constant.StatusExecuting),
)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return "", err
}
if running.ID != "" {
return running.ID, nil
}
installed, err := u.checkCliInstalled()
if err != nil || installed {
return "", err
}
if item.TaskID == "" {
item.TaskID = uuid.NewString()
}
if _, err := taskRepo.GetFirst(taskRepo.WithByID(item.TaskID)); !errors.Is(err, gorm.ErrRecordNotFound) {
if err != nil {
return "", err
}
return "", buserr.New("TaskIsExecuting")
}
if err := NewIContainerService().ContainerCreate(item, true); err != nil {
return "", err
}
return item.TaskID, nil
}
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
if err := updateInstallInfoInDB("redis", req.Database, "password", req.Value); err != nil {
return err
}
remote, err := databaseRepo.Get(repo.WithByName(req.Database))
if err != nil {
return err
}
if remote.From == "local" {
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 *RedisService) UpdatePersistenceConf(req dto.RedisConfPersistenceUpdate) error {
redisInfo, err := appInstallRepo.LoadBaseInfo(req.DBType, req.Database)
if err != nil {
return err
}
var confs []redisConfig
if req.Type == "rbd" {
confs = append(confs, redisConfig{key: "save", value: req.Save})
} else {
confs = append(confs, redisConfig{key: "appendonly", value: req.Appendonly})
confs = append(confs, redisConfig{key: "appendfsync", value: req.Appendfsync})
}
if err := confSet(redisInfo.Name, req.DBType, req.Type, confs); err != nil {
return err
}
if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", global.Dir.AppInstallDir, req.DBType, redisInfo.Name)); err != nil {
return err
}
return nil
}
func (u *RedisService) LoadStatus(req dto.LoadRedisStatus) (*dto.RedisStatus, error) {
redisInfo, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name)
if err != nil {
return nil, err
}
commands := append(redisExec(redisInfo.ContainerName, redisInfo.Password), "info")
cmd := exec.Command("docker", commands...)
stdout, err := cmd.CombinedOutput()
if err != nil {
return nil, errors.New(string(stdout))
}
rows := strings.Split(string(stdout), "\r\n")
rowMap := make(map[string]string)
for _, v := range rows {
itemRow := strings.Split(v, ":")
if len(itemRow) == 2 {
rowMap[itemRow[0]] = itemRow[1]
}
}
var info dto.RedisStatus
arr, err := json.Marshal(rowMap)
if err != nil {
return nil, err
}
_ = json.Unmarshal(arr, &info)
return &info, nil
}
func (u *RedisService) LoadConf(req dto.LoadRedisStatus) (*dto.RedisConf, error) {
redisInfo, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name)
if err != nil {
return nil, err
}
var item dto.RedisConf
item.ContainerName = redisInfo.ContainerName
item.Name = redisInfo.Name
item.Port = redisInfo.Port
item.Requirepass = redisInfo.Password
item.Timeout, _ = configGetStr(redisInfo.ContainerName, redisInfo.Password, "timeout")
item.Maxclients, _ = configGetStr(redisInfo.ContainerName, redisInfo.Password, "maxclients")
item.Maxmemory, _ = configGetStr(redisInfo.ContainerName, redisInfo.Password, "maxmemory")
return &item, nil
}
func (u *RedisService) LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error) {
redisInfo, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name)
if err != nil {
return nil, err
}
var item dto.RedisPersistence
if item.Appendonly, err = configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendonly"); err != nil {
return nil, err
}
if item.Appendfsync, err = configGetStr(redisInfo.ContainerName, redisInfo.Password, "appendfsync"); err != nil {
return nil, err
}
if item.Save, err = configGetStr(redisInfo.ContainerName, redisInfo.Password, "save"); err != nil {
return nil, err
}
return &item, nil
}
func configGetStr(containerName, password, param string) (string, error) {
commands := append(redisExec(containerName, password), []string{"config", "get", param}...)
cmd := exec.Command("docker", commands...)
stdout, err := cmd.CombinedOutput()
if err != nil {
return "", errors.New(string(stdout))
}
rows := strings.Split(string(stdout), "\r\n")
for _, v := range rows {
itemRow := strings.Split(v, "\n")
if len(itemRow) == 3 {
return itemRow[1], nil
}
}
return "", nil
}
type redisConfig struct {
key string
value string
}
func confSet(redisName string, redisType string, updateType string, changeConf []redisConfig) error {
path := fmt.Sprintf("%s/%s/%s/conf/redis.conf", global.Dir.AppInstallDir, redisType, redisName)
lineBytes, err := os.ReadFile(path)
if err != nil {
return err
}
files := strings.Split(string(lineBytes), "\n")
startIndex, endIndex, emptyLine := 0, 0, 0
var newFiles []string
for i := 0; i < len(files); i++ {
if files[i] == "# Redis configuration rewrite by 1Panel" {
startIndex = i
newFiles = append(newFiles, files[i])
continue
}
if files[i] == "# End Redis configuration rewrite by 1Panel" {
endIndex = i
break
}
if startIndex == 0 && strings.HasPrefix(files[i], "save ") {
newFiles = append(newFiles, "# "+files[i])
continue
}
if startIndex != 0 && endIndex == 0 && (len(files[i]) == 0 || (updateType == "rbd" && strings.HasPrefix(files[i], "save "))) {
emptyLine++
continue
}
newFiles = append(newFiles, files[i])
}
if startIndex == 0 {
newFiles = append(newFiles, "# Redis configuration rewrite by 1Panel")
startIndex = len(newFiles) - 1
}
endIndex = endIndex - emptyLine
for _, item := range changeConf {
if item.key == "save" {
saveVal := strings.Split(item.value, ",")
for i := 0; i < len(saveVal); i++ {
newFiles = append(newFiles, "save "+saveVal[i])
}
continue
}
isExist := false
for i := startIndex; i < endIndex; i++ {
if strings.HasPrefix(newFiles[i], item.key) || strings.HasPrefix(newFiles[i], "# "+item.key) {
if item.value == "0" || len(item.value) == 0 {
newFiles[i] = fmt.Sprintf("# %s %s", item.key, item.value)
} else {
newFiles[i] = fmt.Sprintf("%s %s", item.key, item.value)
}
isExist = true
break
}
}
if !isExist {
if item.value == "0" || len(item.value) == 0 {
newFiles = append(newFiles, fmt.Sprintf("# %s %s", item.key, item.value))
} else {
newFiles = append(newFiles, fmt.Sprintf("%s %s", item.key, item.value))
}
}
}
newFiles = append(newFiles, "# End Redis configuration rewrite by 1Panel")
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(newFiles, "\n"))
if err != nil {
return err
}
return nil
}
func redisExec(containerName, password string) []string {
cmds := []string{"exec", containerName, "redis-cli", "-a", password, "--no-auth-warning"}
if len(password) == 0 {
cmds = []string{"exec", containerName, "redis-cli"}
}
return cmds
}