fix: 解决容器终端连接失败的问题

This commit is contained in:
ssongliu 2023-02-24 18:49:34 +08:00 committed by ssongliu
parent 95a5db141f
commit b7324d14dc
12 changed files with 69 additions and 255 deletions

View File

@ -1,16 +1,10 @@
package v1
import (
"context"
"strconv"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/1Panel-dev/1Panel/backend/utils/terminal"
"github.com/docker/docker/api/types"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
@ -231,63 +225,6 @@ func (b *BaseApi) Inspect(c *gin.Context) {
helper.SuccessWithData(c, result)
}
func (b *BaseApi) ContainerExec(c *gin.Context) {
containerID := c.Query("containerid")
command := c.Query("command")
user := c.Query("user")
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
return
}
defer wsConn.Close()
client, err := docker.NewDockerClient()
if wshandleError(wsConn, errors.WithMessage(err, "New docker client failed.")) {
return
}
conf := types.ExecConfig{Tty: true, Cmd: []string{command}, AttachStderr: true, AttachStdin: true, AttachStdout: true}
if len(user) != 0 {
conf.User = user
}
ir, err := client.ContainerExecCreate(context.TODO(), containerID, conf)
if wshandleError(wsConn, errors.WithMessage(err, "failed to set exec conf.")) {
return
}
hr, err := client.ContainerExecAttach(c, ir.ID, types.ExecStartCheck{Detach: false, Tty: true})
if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection.")) {
return
}
defer hr.Close()
sws, err := terminal.NewExecConn(cols, rows, wsConn, hr.Conn)
if wshandleError(wsConn, err) {
return
}
quitChan := make(chan bool, 3)
ctx, cancel := context.WithCancel(context.Background())
sws.Start(ctx, quitChan)
<-quitChan
cancel()
if wshandleError(wsConn, err) {
return
}
}
// @Tags Container
// @Summary Container logs
// @Description 容器日志

View File

@ -2,21 +2,15 @@ package v1
import (
"bufio"
"context"
"fmt"
"os"
"strconv"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/compose"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/1Panel-dev/1Panel/backend/utils/terminal"
"github.com/docker/docker/api/types"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
// @Tags Database Redis
@ -253,64 +247,3 @@ func (b *BaseApi) UpdateRedisConfByFile(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) RedisExec(c *gin.Context) {
redisConf, err := redisService.LoadConf()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
return
}
defer wsConn.Close()
client, err := docker.NewDockerClient()
if wshandleError(wsConn, errors.WithMessage(err, "New docker client failed.")) {
return
}
auth := "redis-cli"
if len(redisConf.Requirepass) != 0 {
auth = fmt.Sprintf("redis-cli -a %s --no-auth-warning", redisConf.Requirepass)
}
conf := types.ExecConfig{Tty: true, Cmd: []string{"bash"}, AttachStderr: true, AttachStdin: true, AttachStdout: true, User: "root"}
ir, err := client.ContainerExecCreate(context.TODO(), redisConf.ContainerName, conf)
if wshandleError(wsConn, errors.WithMessage(err, "failed to set exec conf.")) {
return
}
hr, err := client.ContainerExecAttach(c, ir.ID, types.ExecStartCheck{Detach: false, Tty: true})
if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection.")) {
return
}
defer hr.Close()
sws, err := terminal.NewExecConn(cols, rows, wsConn, hr.Conn, auth)
if wshandleError(wsConn, err) {
return
}
quitChan := make(chan bool, 3)
ctx, cancel := context.WithCancel(context.Background())
sws.Start(ctx, quitChan)
<-quitChan
cancel()
if wshandleError(wsConn, err) {
return
}
}

View File

@ -103,11 +103,63 @@ func (b *BaseApi) RedisWsSsh(c *gin.Context) {
return
}
defer wsConn.Close()
auth := ""
commands := fmt.Sprintf("docker exec -it %s redis-cli", redisConf.ContainerName)
if len(redisConf.Requirepass) != 0 {
auth = fmt.Sprintf("-a %s --no-auth-warning", redisConf.Requirepass)
commands = fmt.Sprintf("docker exec -it %s redis-cli -a %s --no-auth-warning", redisConf.ContainerName, redisConf.Requirepass)
}
slave, err := terminal.NewCommand(redisConf.ContainerName, auth)
slave, err := terminal.NewCommand(commands)
if wshandleError(wsConn, err) {
return
}
defer slave.Close()
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave)
if wshandleError(wsConn, err) {
return
}
quitChan := make(chan bool, 3)
tty.Start(quitChan)
go slave.Wait(quitChan)
<-quitChan
global.LOG.Info("websocket finished")
if wshandleError(wsConn, err) {
return
}
}
func (b *BaseApi) ContainerWsSsh(c *gin.Context) {
containerID := c.Query("containerid")
command := c.Query("command")
user := c.Query("user")
if len(command) == 0 || len(containerID) == 0 {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error param of command or containerID"))
return
}
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
return
}
defer wsConn.Close()
commands := fmt.Sprintf("docker exec -it %s %s", containerID, command)
if len(user) != 0 {
commands = fmt.Sprintf("docker exec -it -u %s %s %s", user, containerID, command)
}
slave, err := terminal.NewCommand(commands)
if wshandleError(wsConn, err) {
return
}

View File

@ -92,10 +92,7 @@ func (u *HostService) SearchForTree(search dto.SearchForTree) ([]dto.HostTree, e
}
func (u *HostService) Create(req dto.HostOperate) (*dto.HostInfo, error) {
host, _ := hostRepo.Get(hostRepo.WithByAddr(req.Addr))
if host.ID != 0 {
return nil, constant.ErrRecordExist
}
var host model.Host
if err := copier.Copy(&host, &req); err != nil {
return nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
}

View File

@ -1,104 +0,0 @@
package terminal
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net"
"sync"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
)
type ExecWsSession struct {
conn net.Conn
wsConn *websocket.Conn
writeMutex sync.Mutex
}
func NewExecConn(cols, rows int, wsConn *websocket.Conn, hijacked net.Conn, commands ...string) (*ExecWsSession, error) {
_, _ = hijacked.Write([]byte(fmt.Sprintf("stty cols %d rows %d && clear \r", cols, rows)))
for _, command := range commands {
_, _ = hijacked.Write([]byte(fmt.Sprintf("%s \r", command)))
}
return &ExecWsSession{
conn: hijacked,
wsConn: wsConn,
}, nil
}
func (sws *ExecWsSession) Start(ctx context.Context, quitChan chan bool) {
go sws.handleSlaveEvent(ctx, quitChan)
go sws.receiveWsMsg(ctx, quitChan)
}
func (sws *ExecWsSession) handleSlaveEvent(ctx context.Context, exitCh chan bool) {
defer setQuit(exitCh)
buffer := make([]byte, 1024)
for {
n, err := sws.conn.Read(buffer)
if err != nil && errors.Is(err, net.ErrClosed) {
return
}
if err := sws.masterWrite(buffer[:n]); err != nil {
if errors.Is(err, websocket.ErrCloseSent) {
return
}
}
}
}
func (sws *ExecWsSession) masterWrite(data []byte) error {
sws.writeMutex.Lock()
defer sws.writeMutex.Unlock()
err := sws.wsConn.WriteMessage(websocket.TextMessage, data)
if err != nil {
return errors.Wrapf(err, "failed to write to master")
}
return nil
}
func (sws *ExecWsSession) receiveWsMsg(ctx context.Context, exitCh chan bool) {
wsConn := sws.wsConn
defer setQuit(exitCh)
for {
_, wsData, err := wsConn.ReadMessage()
if err != nil {
return
}
msgObj := wsMsg{}
_ = json.Unmarshal(wsData, &msgObj)
switch msgObj.Type {
case wsMsgResize:
if msgObj.Cols > 0 && msgObj.Rows > 0 {
sws.ResizeTerminal(msgObj.Rows, msgObj.Cols)
}
case wsMsgCmd:
decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Cmd)
if err != nil {
global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err)
return
}
sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes)
case wsMsgClose:
_, _ = sws.conn.Write([]byte("exit\r"))
return
}
}
}
func (sws *ExecWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) {
_, _ = sws.conn.Write(cmdBytes)
}
func (sws *ExecWsSession) ResizeTerminal(rows int, cols int) {
_, _ = sws.conn.Write([]byte(fmt.Sprintf("stty cols %d rows %d && clear \r", cols, rows)))
}

View File

@ -1,7 +1,6 @@
package terminal
import (
"fmt"
"os"
"os/exec"
"syscall"
@ -27,8 +26,8 @@ type LocalCommand struct {
ptyClosed chan struct{}
}
func NewCommand(containerName string, auth string) (*LocalCommand, error) {
cmd := exec.Command("sh", "-c", fmt.Sprintf("docker exec -it %s redis-cli %s", containerName, auth))
func NewCommand(commands string) (*LocalCommand, error) {
cmd := exec.Command("sh", "-c", commands)
pty, err := pty.Start(cmd)
if err != nil {

View File

@ -35,6 +35,7 @@ func (sws *LocalWsSession) Start(quitChan chan bool) {
func (sws *LocalWsSession) handleSlaveEvent(exitCh chan bool) {
defer setQuit(exitCh)
defer global.LOG.Debug("thread of handle slave event has exited now")
buffer := make([]byte, 1024)
for {
@ -62,6 +63,7 @@ func (sws *LocalWsSession) masterWrite(data []byte) error {
func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) {
wsConn := sws.wsConn
defer setQuit(exitCh)
defer global.LOG.Debug("thread of receive ws msg has exited now")
for {
select {
case <-exitCh:

View File

@ -37,7 +37,6 @@ func (w *safeBuffer) Reset() {
const (
wsMsgCmd = "cmd"
wsMsgResize = "resize"
wsMsgClose = "close"
)
type wsMsg struct {

View File

@ -34,11 +34,11 @@
</el-select>
</el-form-item>
<el-button type="primary" v-if="!terminalOpen" @click="initTerm(formRef)">
<el-button v-if="!terminalOpen" @click="initTerm(formRef)">
{{ $t('commons.button.conn') }}
</el-button>
<el-button type="primary" v-else @click="handleClose()">{{ $t('commons.button.disconn') }}</el-button>
<div style="height: calc(100vh - 290px)" :id="'terminal-exec'"></div>
<el-button v-else @click="handleClose()">{{ $t('commons.button.disconn') }}</el-button>
<div style="height: calc(100vh - 302px)" :id="'terminal-exec'"></div>
</el-form>
</el-drawer>
</template>
@ -180,13 +180,13 @@ const isWsOpen = () => {
};
function handleClose() {
terminalVisiable.value = false;
terminalOpen.value = false;
window.removeEventListener('resize', changeTerminalSize);
if (isWsOpen()) {
terminalSocket && terminalSocket.close();
term.dispose();
}
terminalVisiable.value = false;
terminalOpen.value = false;
}
function changeTerminalSize() {

View File

@ -60,7 +60,7 @@ onUnmounted(() => {
<style lang="scss">
.router_card {
--el-card-border-radius: 8px;
--el-card-padding: 0;
--el-card-padding: 0 !important;
padding: 0px;
padding-bottom: 2px;
padding-top: 2px;

View File

@ -154,7 +154,8 @@ const submitAddHost = (formEl: FormInstance | undefined, ops: string) => {
if (res.data.name.length !== 0) {
title = res.data.name + '-' + title;
}
emit('on-conn-terminal', title, res.data.id, false);
let isLocal = hostInfo.addr === '127.0.0.1';
emit('on-conn-terminal', title, res.data.id, isLocal);
emit('load-host-tree');
}
});

View File

@ -6,6 +6,7 @@
style="background-color: #efefef; margin-top: 20px"
v-model="terminalValue"
:before-leave="beforeLeave"
@tab-change="quickCmd = ''"
@edit="handleTabsRemove"
>
<el-tab-pane
@ -305,19 +306,16 @@ const onConnTerminal = async (title: string, wsID: number, isLocal?: boolean) =>
}
}
}
console.log('走到了这里');
terminalTabs.value.push({
index: tabIndex,
title: title,
wsID: wsID,
status: res.data ? 'online' : 'closed',
});
console.log(terminalTabs.value);
terminalValue.value = tabIndex;
if (!res.data && isLocal) {
dialogRef.value!.acceptParams({ isLocal: true });
}
console.log(terminalValue.value);
nextTick(() => {
ctx.refs[`t-${terminalValue.value}`] &&
ctx.refs[`t-${terminalValue.value}`][0].acceptParams({