Compare commits

...
Author SHA1 Message Date
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
7 changed files with 183 additions and 52 deletions
+10 -4
View File
@@ -86,17 +86,23 @@ func (b *BaseApi) CheckHasCli(c *gin.Context) {
// @Tags Database Redis
// @Summary Install redis-cli
// @Success 200
// @Accept json
// @Param request body dto.RedisCliInstall true "request"
// @Success 200 {string} string
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/redis/install/cli [post]
func (b *BaseApi) InstallCli(c *gin.Context) {
if err := redisService.InstallCli(); err != nil {
var req dto.RedisCliInstall
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := redisService.InstallCli(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
helper.SuccessWithData(c, data)
}
// @Tags Database Redis
+4
View File
@@ -22,6 +22,10 @@ type DBBaseInfo struct {
Port int64 `json:"port"`
}
type RedisCliInstall struct {
TaskID string `json:"taskID" validate:"omitempty,uuid"`
}
// mysql
type MysqlDBSearch struct {
PageInfo
+63 -19
View File
@@ -8,6 +8,12 @@ import (
"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"
@@ -17,12 +23,15 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/compose"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/docker/docker/api/types/container"
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
@@ -33,7 +42,7 @@ type IRedisService interface {
LoadPersistenceConf(req dto.LoadRedisStatus) (*dto.RedisPersistence, error)
CheckHasCli() bool
InstallCli() error
InstallCli(req dto.RedisCliInstall) (string, error)
}
func NewIRedisService() IRedisService {
@@ -61,30 +70,65 @@ func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error {
}
func (u *RedisService) CheckHasCli() bool {
client, err := docker.NewDockerClient()
if err != nil {
return false
}
defer client.Close()
containerLists, err := client.ContainerList(context.Background(), container.ListOptions{})
if err != nil {
return false
}
for _, item := range containerLists {
if strings.ReplaceAll(item.Names[0], "/", "") == "1Panel-redis-cli-tools" {
return true
}
}
return false
installed, _ := u.checkCliInstalled()
return installed
}
func (u *RedisService) InstallCli() error {
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"}},
}
return NewIContainerService().ContainerCreate(item, false)
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 {
+26 -1
View File
@@ -15092,9 +15092,26 @@ const docTemplate = `{
},
"/databases/redis/install/cli": {
"post": {
"consumes": [
"application/json"
],
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.RedisCliInstall"
}
}
],
"responses": {
"200": {
"description": "OK"
"description": "OK",
"schema": {
"type": "string"
}
}
},
"security": [
@@ -40149,6 +40166,14 @@ const docTemplate = `{
],
"type": "object"
},
"dto.RedisCliInstall": {
"type": "object",
"properties": {
"taskID": {
"type": "string"
}
}
},
"dto.RedisConf": {
"properties": {
"containerName": {
+26 -1
View File
@@ -15088,9 +15088,26 @@
},
"/databases/redis/install/cli": {
"post": {
"consumes": [
"application/json"
],
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.RedisCliInstall"
}
}
],
"responses": {
"200": {
"description": "OK"
"description": "OK",
"schema": {
"type": "string"
}
}
},
"security": [
@@ -40145,6 +40162,14 @@
],
"type": "object"
},
"dto.RedisCliInstall": {
"type": "object",
"properties": {
"taskID": {
"type": "string"
}
}
},
"dto.RedisConf": {
"properties": {
"containerName": {
+4 -2
View File
@@ -193,8 +193,10 @@ export const redisPersistenceConf = (type: string, database: string) => {
export const checkRedisCli = () => {
return http.get<boolean>(`/databases/redis/check`);
};
export const installRedisCli = () => {
return http.post(`/databases/redis/install/cli`, {}, TimeoutEnum.T_5M);
export const installRedisCli = (taskID: string, node: string) => {
return http.post<string>(`/databases/redis/install/cli?operateNode=${encodeURIComponent(node)}`, {
taskID,
});
};
export const changeRedisPassword = (database: string, password: string) => {
if (password) {
+50 -25
View File
@@ -81,7 +81,13 @@
:style="{ height: `calc(100vh - ${loadHeight()})`, 'background-color': '#000' }"
:description="loadErrMsg()"
>
<el-button v-if="currentDB.from === 'remote'" v-permission type="primary" @click="installCli">
<el-button
v-if="currentDB.from === 'remote'"
v-permission
type="primary"
:loading="cliSubmitting"
@click="installCli"
>
{{ $t('commons.button.enable') }}
</el-button>
</el-empty>
@@ -134,6 +140,7 @@
</DialogPro>
<QuickCmd ref="dialogQuickCmdRef" @reload="loadQuickCmd" />
<TaskLog ref="cliTaskLogRef" @close="checkCliValid" />
</div>
</template>
@@ -148,12 +155,14 @@ import { checkAppInstalled } from '@/api/modules/app';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { listDatabases, checkRedisCli, installRedisCli } from '@/api/modules/database';
import { Database } from '@/api/interface/database';
import { MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
import { getCommandList } from '@/api/modules/command';
import { routerToName, routerToNameWithQuery } from '@/utils/router';
import { useOperateNodeContext } from '@/composables/useOperateNodeContext';
import TaskLog from '@/components/log/task/index.vue';
import { newUUID } from '@/utils/id';
const { currentNode, currentRedisDB, openMenuTabs } = useGlobalStore();
useOperateNodeContext(currentNode);
@@ -170,7 +179,11 @@ const appStatusRef = ref();
const open = ref(false);
const redisCliExist = ref();
const redisCliExist = ref(false);
const cliSubmitting = ref(false);
const cliTaskLogRef = ref<InstanceType<typeof TaskLog>>();
const operateNode = currentNode.value;
let disposed = false;
const appKey = ref('redis');
const appName = ref();
@@ -250,6 +263,7 @@ const changeDatabase = async () => {
const loadDBOptions = async () => {
try {
const res = await listDatabases('redis,redis-cluster');
if (disposed || operateNode !== currentNode.value) return;
let datas = res.data || [];
dbOptionsLocal.value = [];
dbOptionsRemote.value = [];
@@ -286,7 +300,7 @@ const loadDBOptions = async () => {
reOpenTerminal();
}
} finally {
isLoaded.value = true;
if (!disposed && operateNode === currentNode.value) isLoaded.value = true;
}
};
@@ -301,6 +315,9 @@ const reOpenTerminal = async () => {
};
const initTerminal = async () => {
if (disposed || !currentDB.value) return;
const database = currentDBName.value;
const isCurrent = () => !disposed && operateNode === currentNode.value && database === currentDBName.value;
loading.value = true;
if (currentDB.value.from === 'remote') {
if (!redisCliExist.value) {
@@ -311,9 +328,10 @@ const initTerminal = async () => {
loading.value = false;
redisIsExist.value = true;
nextTick(() => {
if (!isCurrent()) return;
terminalShow.value = true;
redisStatus.value = 'Running';
terminalRef.value.acceptParams({
terminalRef.value?.acceptParams({
endpoint: '/api/v2/hosts/terminal/container',
args: `source=redis&name=${currentDBName.value}&from=${currentDB.value.from}`,
error: '',
@@ -325,13 +343,15 @@ const initTerminal = async () => {
}
await checkAppInstalled(currentDB.value.type, currentDBName.value)
.then((res) => {
if (!isCurrent()) return;
redisIsExist.value = res.data.isExist;
redisStatus.value = res.data.status;
loading.value = false;
nextTick(() => {
if (!isCurrent()) return;
if (res.data.status === 'Running') {
terminalShow.value = true;
terminalRef.value.acceptParams({
terminalRef.value?.acceptParams({
endpoint: '/api/v2/hosts/terminal/container',
args: `source=${currentDB.value.type}&name=${currentDBName.value}&from=${currentDB.value.from}`,
error: '',
@@ -342,6 +362,7 @@ const initTerminal = async () => {
isRefresh.value = !isRefresh.value;
})
.catch(() => {
if (!isCurrent()) return;
closeTerminal(false);
loading.value = false;
});
@@ -353,27 +374,30 @@ const closeTerminal = async (isKeepShow: boolean) => {
};
const checkCliValid = async () => {
await checkRedisCli()
.then((res) => {
redisCliExist.value = res.data;
loadDBOptions();
})
.catch(() => {
loadDBOptions();
});
if (disposed || operateNode !== currentNode.value) return;
try {
const res = await checkRedisCli();
if (disposed || operateNode !== currentNode.value) return;
redisCliExist.value = res.data;
} finally {
if (!disposed && operateNode === currentNode.value) await loadDBOptions();
}
};
const installCli = async () => {
loading.value = true;
await installRedisCli()
.then(() => {
loading.value = false;
redisCliExist.value = true;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
reOpenTerminal();
})
.catch(() => {
loading.value = false;
});
if (cliSubmitting.value) return;
cliSubmitting.value = true;
try {
const res = await installRedisCli(newUUID(), operateNode);
if (disposed || operateNode !== currentNode.value) return;
if (res.data) {
cliTaskLogRef.value?.openWithTaskID(res.data, true, operateNode);
} else {
await checkCliValid();
}
} finally {
cliSubmitting.value = false;
}
};
const loadQuickCmd = async () => {
@@ -403,6 +427,7 @@ const onAfter = () => {
initTerminal();
};
onBeforeUnmount(() => {
disposed = true;
closeTerminal(false);
});
</script>