Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 40 additions & 55 deletions agent/app/api/v2/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"time"

"github.com/1Panel-dev/1Panel/agent/app/dto"
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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"}...)
Expand All @@ -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 {
Expand All @@ -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,
Expand Down
17 changes: 16 additions & 1 deletion agent/utils/terminal/local_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions frontend/src/components/terminal/database.vue
Original file line number Diff line number Diff line change
@@ -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>
10 changes: 10 additions & 0 deletions frontend/src/views/database/mysql/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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') }}
Expand Down Expand Up @@ -254,6 +257,7 @@
<AppResources ref="checkRef"></AppResources>
<DeleteDialog ref="deleteRef" @search="search" />
<PortJumpDialog ref="dialogPortJumpRef" />
<TerminalDialog ref="dialogTerminalRef" />
</div>
</template>

Expand All @@ -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';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 () => {
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/views/database/postgresql/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -221,6 +224,7 @@
<DeleteDialog ref="deleteRef" @search="search" />

<PortJumpDialog ref="dialogPortJumpRef" />
<TerminalDialog ref="dialogTerminalRef" />
</div>
</template>

Expand All @@ -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';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading