mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-12-19 05:49:02 +08:00
parent
58c994a3e2
commit
a33b17d5da
5 changed files with 132 additions and 56 deletions
|
|
@ -6,7 +6,6 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
|
|
@ -86,15 +85,16 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
source := c.Query("source")
|
||||
var containerID string
|
||||
var initCmd []string
|
||||
switch source {
|
||||
case "redis", "redis-cluster":
|
||||
containerID, initCmd, err = loadRedisInitCmd(c, source)
|
||||
initCmd, err = loadRedisInitCmd(c, source)
|
||||
case "ollama":
|
||||
containerID, initCmd, err = loadOllamaInitCmd(c)
|
||||
initCmd, err = loadOllamaInitCmd(c)
|
||||
case "container":
|
||||
containerID, initCmd, err = loadContainerInitCmd(c)
|
||||
initCmd, err = loadContainerInitCmd(c)
|
||||
case "database":
|
||||
initCmd, err = loadDatabaseInitCmd(c)
|
||||
default:
|
||||
if wshandleError(wsConn, fmt.Errorf("not support such source %s", source)) {
|
||||
return
|
||||
|
|
@ -103,12 +103,10 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
|
|||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
pidMap := loadMapFromDockerTop(containerID)
|
||||
slave, err := terminal.NewCommand("docker", initCmd...)
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
defer killBash(containerID, strings.ReplaceAll(strings.Join(initCmd, " "), fmt.Sprintf("exec -it %s ", containerID), ""), pidMap)
|
||||
defer slave.Close()
|
||||
|
||||
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
|
||||
|
|
@ -127,18 +125,18 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
|
|||
_ = wsConn.WriteControl(websocket.CloseMessage, nil, dt)
|
||||
}
|
||||
|
||||
func loadRedisInitCmd(c *gin.Context, redisType string) (string, []string, error) {
|
||||
func loadRedisInitCmd(c *gin.Context, redisType string) ([]string, error) {
|
||||
name := c.Query("name")
|
||||
from := c.Query("from")
|
||||
commands := []string{"exec", "-it"}
|
||||
database, err := databaseService.Get(name)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("no such database in db, err: %v", err)
|
||||
return nil, fmt.Errorf("no such database in db, err: %v", err)
|
||||
}
|
||||
if from == "local" {
|
||||
redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: name, Type: redisType})
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("no such app in db, err: %v", err)
|
||||
return nil, fmt.Errorf("no such app in db, err: %v", err)
|
||||
}
|
||||
name = redisInfo.ContainerName
|
||||
commands = append(commands, []string{name, "redis-cli"}...)
|
||||
|
|
@ -152,38 +150,61 @@ func loadRedisInitCmd(c *gin.Context, redisType string) (string, []string, error
|
|||
commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...)
|
||||
}
|
||||
}
|
||||
return name, commands, nil
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
func loadOllamaInitCmd(c *gin.Context) (string, []string, error) {
|
||||
func loadOllamaInitCmd(c *gin.Context) ([]string, error) {
|
||||
name := c.Query("name")
|
||||
if cmd.CheckIllegal(name) {
|
||||
return "", nil, fmt.Errorf("ollama model %s contains illegal characters", name)
|
||||
return nil, fmt.Errorf("ollama model %s contains illegal characters", name)
|
||||
}
|
||||
ollamaInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: "", Type: "ollama"})
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("no such app in db, err: %v", err)
|
||||
return nil, fmt.Errorf("no such app in db, err: %v", err)
|
||||
}
|
||||
containerName := ollamaInfo.ContainerName
|
||||
return containerName, []string{"exec", "-it", containerName, "ollama", "run", name}, nil
|
||||
return []string{"exec", "-it", containerName, "ollama", "run", name}, nil
|
||||
}
|
||||
|
||||
func loadContainerInitCmd(c *gin.Context) (string, []string, error) {
|
||||
func loadContainerInitCmd(c *gin.Context) ([]string, error) {
|
||||
containerID := c.Query("containerid")
|
||||
command := c.Query("command")
|
||||
user := c.Query("user")
|
||||
if cmd.CheckIllegal(user, containerID, command) {
|
||||
return "", nil, fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
|
||||
return nil, fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
|
||||
}
|
||||
if len(command) == 0 || len(containerID) == 0 {
|
||||
return "", nil, fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
|
||||
return nil, fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
|
||||
}
|
||||
commands := []string{"exec", "-it", containerID, command}
|
||||
if len(user) != 0 {
|
||||
commands = []string{"exec", "-it", "-u", user, containerID, command}
|
||||
}
|
||||
|
||||
return containerID, commands, nil
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
func loadDatabaseInitCmd(c *gin.Context) ([]string, error) {
|
||||
database := c.Query("database")
|
||||
databaseType := c.Query("databaseType")
|
||||
if len(database) == 0 || len(databaseType) == 0 {
|
||||
return nil, fmt.Errorf("error param of database: %s or database type: %s", database, databaseType)
|
||||
}
|
||||
databaseConn, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Type: databaseType, Name: database})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no such database in db, err: %v", err)
|
||||
}
|
||||
commands := []string{"exec", "-it", databaseConn.ContainerName}
|
||||
switch databaseType {
|
||||
case "mysql", "mysql-cluster":
|
||||
commands = append(commands, []string{"mysql", "-uroot", "-p" + databaseConn.Password}...)
|
||||
case "mariadb":
|
||||
commands = append(commands, []string{"mariadb", "-uroot", "-p" + databaseConn.Password}...)
|
||||
case "postgresql", "postgresql-cluster":
|
||||
commands = []string{"exec", "-e", fmt.Sprintf("PGPASSWORD=%s", databaseConn.Password), "-it", databaseConn.ContainerName, "psql", "-t", "-U", databaseConn.Username}
|
||||
}
|
||||
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
func wshandleError(ws *websocket.Conn, err error) bool {
|
||||
|
|
@ -206,42 +227,6 @@ func wshandleError(ws *websocket.Conn, err error) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func loadMapFromDockerTop(containerID string) map[string]string {
|
||||
pidMap := make(map[string]string)
|
||||
sudo := cmd.SudoHandleCmd()
|
||||
|
||||
stdout, err := cmd.RunDefaultWithStdoutBashCf("%s docker top %s -eo pid,command ", sudo, containerID)
|
||||
if err != nil {
|
||||
return pidMap
|
||||
}
|
||||
lines := strings.Split(stdout, "\n")
|
||||
for _, line := range lines {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
pidMap[parts[0]] = strings.Join(parts[1:], " ")
|
||||
}
|
||||
return pidMap
|
||||
}
|
||||
|
||||
func killBash(containerID, comm string, pidMap map[string]string) {
|
||||
sudo := cmd.SudoHandleCmd()
|
||||
newPidMap := loadMapFromDockerTop(containerID)
|
||||
for pid, command := range newPidMap {
|
||||
isOld := false
|
||||
for pid2 := range pidMap {
|
||||
if pid == pid2 {
|
||||
isOld = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isOld && command == comm {
|
||||
_, _ = cmd.RunDefaultWithStdoutBashCf("%s kill -9 %s", sudo, pid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var upGrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024 * 1024 * 10,
|
||||
|
|
|
|||
|
|
@ -60,8 +60,23 @@ func (lcmd *LocalCommand) Write(p []byte) (n int, err error) {
|
|||
}
|
||||
|
||||
func (lcmd *LocalCommand) Close() error {
|
||||
if lcmd.pty != nil {
|
||||
lcmd.pty.Write([]byte{3})
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
lcmd.pty.Write([]byte{4})
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
lcmd.pty.Write([]byte("exit\n"))
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
if lcmd.cmd != nil && lcmd.cmd.Process != nil {
|
||||
_ = lcmd.cmd.Process.Kill()
|
||||
lcmd.cmd.Process.Signal(syscall.SIGTERM)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if lcmd.cmd.ProcessState == nil || !lcmd.cmd.ProcessState.Exited() {
|
||||
lcmd.cmd.Process.Kill()
|
||||
}
|
||||
}
|
||||
_ = lcmd.pty.Close()
|
||||
return nil
|
||||
|
|
|
|||
56
frontend/src/components/terminal/database.vue
Normal file
56
frontend/src/components/terminal/database.vue
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<DrawerPro
|
||||
v-model="open"
|
||||
:header="$t('menu.terminal')"
|
||||
@close="handleClose"
|
||||
:resource="database"
|
||||
:autoClose="!open"
|
||||
size="large"
|
||||
:fullScreen="true"
|
||||
>
|
||||
<template #content>
|
||||
<Terminal style="height: calc(100vh - 100px)" ref="terminalRef"></Terminal>
|
||||
</template>
|
||||
</DrawerPro>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick } from 'vue';
|
||||
import Terminal from '@/components/terminal/index.vue';
|
||||
|
||||
const open = ref(false);
|
||||
const terminalRef = ref<InstanceType<typeof Terminal> | null>(null);
|
||||
const database = ref();
|
||||
const databaseType = ref();
|
||||
|
||||
interface DialogProps {
|
||||
databaseType: string;
|
||||
database: string;
|
||||
}
|
||||
const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
database.value = params.database;
|
||||
databaseType.value = params.databaseType;
|
||||
open.value = false;
|
||||
await initTerm();
|
||||
};
|
||||
|
||||
const initTerm = async () => {
|
||||
open.value = true;
|
||||
await nextTick();
|
||||
terminalRef.value!.acceptParams({
|
||||
endpoint: '/api/v2/containers/exec',
|
||||
args: `source=database&databaseType=${databaseType.value}&database=${database.value}`,
|
||||
error: '',
|
||||
initCmd: '',
|
||||
});
|
||||
};
|
||||
|
||||
function handleClose() {
|
||||
terminalRef.value?.onClose();
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
|
@ -46,6 +46,9 @@
|
|||
<el-button @click="goRemoteDB()" type="primary" plain>
|
||||
{{ $t('database.remoteDB') }}
|
||||
</el-button>
|
||||
<el-button @click="goTerminal()" :disabled="currentDB?.from !== 'local'" type="primary" plain>
|
||||
{{ $t('menu.terminal') }}
|
||||
</el-button>
|
||||
<el-dropdown>
|
||||
<el-button type="primary" plain>
|
||||
{{ $t('database.manage') }}
|
||||
|
|
@ -254,6 +257,7 @@
|
|||
<AppResources ref="checkRef"></AppResources>
|
||||
<DeleteDialog ref="deleteRef" @search="search" />
|
||||
<PortJumpDialog ref="dialogPortJumpRef" />
|
||||
<TerminalDialog ref="dialogTerminalRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -263,6 +267,7 @@ import OperateDialog from '@/views/database/mysql/create/index.vue';
|
|||
import DeleteDialog from '@/views/database/mysql/delete/index.vue';
|
||||
import PasswordDialog from '@/views/database/mysql/password/index.vue';
|
||||
import RootPasswordDialog from '@/views/database/mysql/conn/index.vue';
|
||||
import TerminalDialog from '@/components/terminal/database.vue';
|
||||
import AppResources from '@/views/database/mysql/check/index.vue';
|
||||
import AppStatus from '@/components/app-status/index.vue';
|
||||
import Backups from '@/components/backup/index.vue';
|
||||
|
|
@ -307,6 +312,7 @@ const currentDBName = ref();
|
|||
const bindRef = ref();
|
||||
const checkRef = ref();
|
||||
const deleteRef = ref();
|
||||
const dialogTerminalRef = ref();
|
||||
|
||||
const phpadminPort = ref();
|
||||
const adminerPort = ref();
|
||||
|
|
@ -371,6 +377,10 @@ const goRemoteDB = async () => {
|
|||
routerToName('MySQL-Remote');
|
||||
};
|
||||
|
||||
const goTerminal = () => {
|
||||
dialogTerminalRef.value.acceptParams({ databaseType: currentDB.value.type, database: currentDB.value.database });
|
||||
};
|
||||
|
||||
const passwordRef = ref();
|
||||
|
||||
const onSetting = async () => {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@
|
|||
<el-button @click="goRemoteDB" type="primary" plain>
|
||||
{{ $t('database.remoteDB') }}
|
||||
</el-button>
|
||||
<el-button @click="goTerminal()" :disabled="currentDB?.from !== 'local'" type="primary" plain>
|
||||
{{ $t('menu.terminal') }}
|
||||
</el-button>
|
||||
<el-button @click="goDashboard()" type="primary" plain>PGAdmin4</el-button>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
|
|
@ -221,6 +224,7 @@
|
|||
<DeleteDialog ref="deleteRef" @search="search" />
|
||||
|
||||
<PortJumpDialog ref="dialogPortJumpRef" />
|
||||
<TerminalDialog ref="dialogTerminalRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -229,6 +233,7 @@ import BindDialog from '@/views/database/postgresql/bind/index.vue';
|
|||
import OperateDialog from '@/views/database/postgresql/create/index.vue';
|
||||
import DeleteDialog from '@/views/database/postgresql/delete/index.vue';
|
||||
import PasswordDialog from '@/views/database/postgresql/password/index.vue';
|
||||
import TerminalDialog from '@/components/terminal/database.vue';
|
||||
import PrivilegesDialog from '@/views/database/postgresql/privileges/index.vue';
|
||||
import RootPasswordDialog from '@/views/database/postgresql/conn/index.vue';
|
||||
import AppResources from '@/views/database/postgresql/check/index.vue';
|
||||
|
|
@ -275,6 +280,7 @@ const checkRef = ref();
|
|||
const deleteRef = ref();
|
||||
const bindRef = ref();
|
||||
const privilegesRef = ref();
|
||||
const dialogTerminalRef = ref();
|
||||
|
||||
const pgadminPort = ref();
|
||||
const dashboardName = ref();
|
||||
|
|
@ -330,6 +336,10 @@ const goRemoteDB = async () => {
|
|||
routerToName('PostgreSQL-Remote');
|
||||
};
|
||||
|
||||
const goTerminal = () => {
|
||||
dialogTerminalRef.value.acceptParams({ databaseType: currentDB.value.type, database: currentDB.value.database });
|
||||
};
|
||||
|
||||
const passwordRef = ref();
|
||||
|
||||
const onSetting = async () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue