diff --git a/agent/app/api/v2/database_redis.go b/agent/app/api/v2/database_redis.go index 34ba7b62e..a2a01e3e7 100644 --- a/agent/app/api/v2/database_redis.go +++ b/agent/app/api/v2/database_redis.go @@ -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 diff --git a/agent/app/dto/database.go b/agent/app/dto/database.go index e28cf87dc..945eb2a6b 100644 --- a/agent/app/dto/database.go +++ b/agent/app/dto/database.go @@ -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 diff --git a/agent/app/service/database_redis.go b/agent/app/service/database_redis.go index c895cfab7..822b80783 100644 --- a/agent/app/service/database_redis.go +++ b/agent/app/service/database_redis.go @@ -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 { diff --git a/core/cmd/server/docs/docs.go b/core/cmd/server/docs/docs.go index f157ad5d6..605b42eb4 100644 --- a/core/cmd/server/docs/docs.go +++ b/core/cmd/server/docs/docs.go @@ -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": { diff --git a/core/cmd/server/docs/swagger.json b/core/cmd/server/docs/swagger.json index 56fb0cd24..ee27df5aa 100644 --- a/core/cmd/server/docs/swagger.json +++ b/core/cmd/server/docs/swagger.json @@ -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": { diff --git a/frontend/src/api/modules/database.ts b/frontend/src/api/modules/database.ts index 08910a100..bb06fb66e 100644 --- a/frontend/src/api/modules/database.ts +++ b/frontend/src/api/modules/database.ts @@ -193,8 +193,10 @@ export const redisPersistenceConf = (type: string, database: string) => { export const checkRedisCli = () => { return http.get(`/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(`/databases/redis/install/cli?operateNode=${encodeURIComponent(node)}`, { + taskID, + }); }; export const changeRedisPassword = (database: string, password: string) => { if (password) { diff --git a/frontend/src/views/database/redis/index.vue b/frontend/src/views/database/redis/index.vue index fbdc19366..6bb2e571f 100644 --- a/frontend/src/views/database/redis/index.vue +++ b/frontend/src/views/database/redis/index.vue @@ -81,7 +81,13 @@ :style="{ height: `calc(100vh - ${loadHeight()})`, 'background-color': '#000' }" :description="loadErrMsg()" > - + {{ $t('commons.button.enable') }} @@ -134,6 +140,7 @@ + @@ -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>(); +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); });