feat: 完成容器终端功能

This commit is contained in:
ssongliu 2022-10-18 18:39:45 +08:00 committed by ssongliu
parent 3ca1e97469
commit e58e5cef0d
36 changed files with 848 additions and 534 deletions

View File

@ -1,10 +1,10 @@
package v1
import (
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"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/gin-gonic/gin"
)

View File

@ -1,13 +1,18 @@
package v1
import (
"errors"
"context"
"strconv"
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"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"
)
func (b *BaseApi) SearchContainer(c *gin.Context) {
@ -158,6 +163,59 @@ 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, 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
}
}
func (b *BaseApi) ContainerLogs(c *gin.Context) {
var req dto.ContainerLog
if err := c.ShouldBindJSON(&req); err != nil {

View File

@ -1,10 +1,10 @@
package v1
import (
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"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/gin-gonic/gin"
)

View File

@ -1,10 +1,10 @@
package v1
import (
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"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/gin-gonic/gin"
)

View File

@ -112,6 +112,7 @@ type BatchDelete struct {
type ComposeInfo struct {
Name string `json:"name"`
CreatedAt string `json:"createdAt"`
CreatedBy string `json:"createdBy"`
ContainerNumber int `json:"containerNumber"`
ConfigFile string `json:"configFile"`
Workdir string `json:"workdir"`

View File

@ -1,8 +1,8 @@
package repo
import (
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/global"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/global"
)
type ComposeTemplateRepo struct{}

View File

@ -1,8 +1,8 @@
package repo
import (
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/global"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/global"
)
type ImageRepoRepo struct{}

View File

@ -4,6 +4,16 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"math"
"net/http"
"os"
"path"
"reflect"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant"
@ -15,15 +25,6 @@ import (
"github.com/joho/godotenv"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
"io/ioutil"
"math"
"net/http"
"os"
"path"
"reflect"
"strconv"
"strings"
"time"
)
type DatabaseOp string

View File

@ -1,8 +1,8 @@
package service
import (
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
)

View File

@ -1,28 +1,23 @@
package service
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"github.com/1Panel-dev/1Panel/utils/docker"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-connections/nat"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
@ -30,10 +25,6 @@ import (
type ContainerService struct{}
const composeProjectLabel = "com.docker.compose.project"
const composeConfigLabel = "com.docker.compose.project.config_files"
const composeWorkdirLabel = "com.docker.compose.project.working_dir"
type IContainerService interface {
Page(req dto.PageContainer) (int64, interface{}, error)
PageNetwork(req dto.PageInfo) (int64, interface{}, error)
@ -101,128 +92,6 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
return int64(total), backDatas, nil
}
func (u *ContainerService) PageCompose(req dto.PageInfo) (int64, interface{}, error) {
var (
records []dto.ComposeInfo
BackDatas []dto.ComposeInfo
)
client, err := docker.NewDockerClient()
if err != nil {
return 0, nil, err
}
options := types.ContainerListOptions{All: true}
options.Filters = filters.NewArgs()
options.Filters.Add("label", composeProjectLabel)
list, err := client.ContainerList(context.Background(), options)
if err != nil {
return 0, nil, err
}
composeMap := make(map[string]dto.ComposeInfo)
for _, container := range list {
if name, ok := container.Labels[composeProjectLabel]; ok {
containerItem := dto.ComposeContainer{
ContainerID: container.ID,
Name: container.Names[0][1:],
State: container.State,
CreateTime: time.Unix(container.Created, 0).Format("2006-01-02 15:04:05"),
}
if compose, has := composeMap[name]; has {
compose.ContainerNumber++
compose.Containers = append(compose.Containers, containerItem)
composeMap[name] = compose
} else {
config := container.Labels[composeConfigLabel]
workdir := container.Labels[composeWorkdirLabel]
composeItem := dto.ComposeInfo{
ContainerNumber: 1,
CreatedAt: time.Unix(container.Created, 0).Format("2006-01-02 15:04:05"),
ConfigFile: config,
Workdir: workdir,
Containers: []dto.ComposeContainer{containerItem},
}
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
composeItem.Path = config
} else {
composeItem.Path = workdir
}
composeMap[name] = composeItem
}
}
}
for key, value := range composeMap {
value.Name = key
records = append(records, value)
}
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
BackDatas = make([]dto.ComposeInfo, 0)
} else {
if end >= total {
end = total
}
BackDatas = records[start:end]
}
return int64(total), BackDatas, nil
}
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
if req.From == "template" {
template, err := composeRepo.Get(commonRepo.WithByID(req.Template))
if err != nil {
return err
}
req.From = template.From
if req.From == "edit" {
req.File = template.Content
} else {
req.Path = template.Path
}
}
if req.From == "edit" {
dir := fmt.Sprintf("%s/%s", constant.TmpComposeBuildDir, req.Name)
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
}
path := fmt.Sprintf("%s/docker-compose.yml", dir)
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
return err
}
defer file.Close()
write := bufio.NewWriter(file)
_, _ = write.WriteString(string(req.File))
write.Flush()
req.Path = path
}
go func() {
cmd := exec.Command("docker-compose", "-f", req.Path, "up", "-d")
stdout, err := cmd.CombinedOutput()
if err != nil {
global.LOG.Debugf("docker-compose up %s failed, err: %v", req.Name, err)
return
}
global.LOG.Debugf("docker-compose up %s successful, logs: %v", req.Name, string(stdout))
}()
return nil
}
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
cmd := exec.Command("docker-compose", "-f", req.Path, req.Operation)
stdout, err := cmd.CombinedOutput()
if err != nil {
return err
}
global.LOG.Debugf("docker-compose %s %s successful: logs: %v", req.Operation, req.Path, string(stdout))
return err
}
func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
client, err := docker.NewDockerClient()
if err != nil {
@ -387,187 +256,6 @@ func (u *ContainerService) ContainerStats(id string) (*dto.ContainterStats, erro
return &data, nil
}
func (u *ContainerService) PageNetwork(req dto.PageInfo) (int64, interface{}, error) {
client, err := docker.NewDockerClient()
if err != nil {
return 0, nil, err
}
list, err := client.NetworkList(context.TODO(), types.NetworkListOptions{})
if err != nil {
return 0, nil, err
}
var (
data []dto.Network
records []types.NetworkResource
)
total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
records = make([]types.NetworkResource, 0)
} else {
if end >= total {
end = total
}
records = list[start:end]
}
for _, item := range records {
tag := make([]string, 0)
for key, val := range item.Labels {
tag = append(tag, fmt.Sprintf("%s=%s", key, val))
}
var ipam network.IPAMConfig
if len(item.IPAM.Config) > 0 {
ipam = item.IPAM.Config[0]
}
data = append(data, dto.Network{
ID: item.ID,
CreatedAt: item.Created,
Name: item.Name,
Driver: item.Driver,
IPAMDriver: item.IPAM.Driver,
Subnet: ipam.Subnet,
Gateway: ipam.Gateway,
Attachable: item.Attachable,
Labels: tag,
})
}
return int64(total), data, nil
}
func (u *ContainerService) DeleteNetwork(req dto.BatchDelete) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
for _, id := range req.Ids {
if err := client.NetworkRemove(context.TODO(), id); err != nil {
return err
}
}
return nil
}
func (u *ContainerService) CreateNetwork(req dto.NetworkCreat) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
var (
ipam network.IPAMConfig
hasConf bool
)
if len(req.Subnet) != 0 {
ipam.Subnet = req.Subnet
hasConf = true
}
if len(req.Gateway) != 0 {
ipam.Gateway = req.Gateway
hasConf = true
}
if len(req.IPRange) != 0 {
ipam.IPRange = req.IPRange
hasConf = true
}
options := types.NetworkCreate{
Driver: req.Driver,
Options: stringsToMap(req.Options),
Labels: stringsToMap(req.Labels),
}
if hasConf {
options.IPAM = &network.IPAM{Config: []network.IPAMConfig{ipam}}
}
if _, err := client.NetworkCreate(context.TODO(), req.Name, options); err != nil {
return err
}
return nil
}
func (u *ContainerService) PageVolume(req dto.PageInfo) (int64, interface{}, error) {
client, err := docker.NewDockerClient()
if err != nil {
return 0, nil, err
}
list, err := client.VolumeList(context.TODO(), filters.NewArgs())
if err != nil {
return 0, nil, err
}
var (
data []dto.Volume
records []*types.Volume
)
total, start, end := len(list.Volumes), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
records = make([]*types.Volume, 0)
} else {
if end >= total {
end = total
}
records = list.Volumes[start:end]
}
for _, item := range records {
tag := make([]string, 0)
for _, val := range item.Labels {
tag = append(tag, val)
}
createTime, _ := time.Parse("2006-01-02T15:04:05Z", item.CreatedAt)
data = append(data, dto.Volume{
CreatedAt: createTime,
Name: item.Name,
Driver: item.Driver,
Mountpoint: item.Mountpoint,
Labels: tag,
})
}
return int64(total), data, nil
}
func (u *ContainerService) ListVolume() ([]dto.Options, error) {
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
list, err := client.VolumeList(context.TODO(), filters.NewArgs())
if err != nil {
return nil, err
}
var data []dto.Options
for _, item := range list.Volumes {
data = append(data, dto.Options{
Option: item.Name,
})
}
return data, nil
}
func (u *ContainerService) DeleteVolume(req dto.BatchDelete) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
for _, id := range req.Ids {
if err := client.VolumeRemove(context.TODO(), id, true); err != nil {
return err
}
}
return nil
}
func (u *ContainerService) CreateVolume(req dto.VolumeCreat) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
options := volume.VolumeCreateBody{
Name: req.Name,
Driver: req.Driver,
DriverOpts: stringsToMap(req.Options),
Labels: stringsToMap(req.Labels),
}
if _, err := client.VolumeCreate(context.TODO(), options); err != nil {
return err
}
return nil
}
func stringsToMap(list []string) map[string]string {
var lableMap = make(map[string]string)
for _, label := range list {

View File

@ -0,0 +1,149 @@
package service
import (
"bufio"
"fmt"
"os"
"os/exec"
"strings"
"time"
"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/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"golang.org/x/net/context"
)
const composeProjectLabel = "com.docker.compose.project"
const composeConfigLabel = "com.docker.compose.project.config_files"
const composeWorkdirLabel = "com.docker.compose.project.working_dir"
const composeCreatedBy = "createdBy"
func (u *ContainerService) PageCompose(req dto.PageInfo) (int64, interface{}, error) {
var (
records []dto.ComposeInfo
BackDatas []dto.ComposeInfo
)
client, err := docker.NewDockerClient()
if err != nil {
return 0, nil, err
}
options := types.ContainerListOptions{All: true}
options.Filters = filters.NewArgs()
options.Filters.Add("label", composeProjectLabel)
list, err := client.ContainerList(context.Background(), options)
if err != nil {
return 0, nil, err
}
composeMap := make(map[string]dto.ComposeInfo)
for _, container := range list {
if name, ok := container.Labels[composeProjectLabel]; ok {
containerItem := dto.ComposeContainer{
ContainerID: container.ID,
Name: container.Names[0][1:],
State: container.State,
CreateTime: time.Unix(container.Created, 0).Format("2006-01-02 15:04:05"),
}
if compose, has := composeMap[name]; has {
compose.ContainerNumber++
compose.Containers = append(compose.Containers, containerItem)
composeMap[name] = compose
} else {
config := container.Labels[composeConfigLabel]
workdir := container.Labels[composeWorkdirLabel]
composeItem := dto.ComposeInfo{
ContainerNumber: 1,
CreatedAt: time.Unix(container.Created, 0).Format("2006-01-02 15:04:05"),
ConfigFile: config,
Workdir: workdir,
Containers: []dto.ComposeContainer{containerItem},
}
createdBy, ok := container.Labels[composeCreatedBy]
if ok {
composeItem.CreatedBy = createdBy
}
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
composeItem.Path = config
} else {
composeItem.Path = workdir
}
composeMap[name] = composeItem
}
}
}
for key, value := range composeMap {
value.Name = key
records = append(records, value)
}
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
BackDatas = make([]dto.ComposeInfo, 0)
} else {
if end >= total {
end = total
}
BackDatas = records[start:end]
}
return int64(total), BackDatas, nil
}
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
if req.From == "template" {
template, err := composeRepo.Get(commonRepo.WithByID(req.Template))
if err != nil {
return err
}
req.From = template.From
if req.From == "edit" {
req.File = template.Content
} else {
req.Path = template.Path
}
}
if req.From == "edit" {
dir := fmt.Sprintf("%s/%s", constant.TmpComposeBuildDir, req.Name)
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
}
path := fmt.Sprintf("%s/docker-compose.yml", dir)
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
return err
}
defer file.Close()
write := bufio.NewWriter(file)
_, _ = write.WriteString(string(req.File))
write.Flush()
req.Path = path
}
go func() {
cmd := exec.Command("docker-compose", "-f", req.Path, "up", "-d")
stdout, err := cmd.CombinedOutput()
if err != nil {
global.LOG.Debugf("docker-compose up %s failed, err: %v", req.Name, err)
return
}
global.LOG.Debugf("docker-compose up %s successful, logs: %v", req.Name, string(stdout))
}()
return nil
}
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
cmd := exec.Command("docker-compose", "-f", req.Path, req.Operation)
stdout, err := cmd.CombinedOutput()
if err != nil {
return err
}
global.LOG.Debugf("docker-compose %s %s successful: logs: %v", req.Operation, req.Path, string(stdout))
return err
}

View File

@ -0,0 +1,106 @@
package service
import (
"context"
"fmt"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
)
func (u *ContainerService) PageNetwork(req dto.PageInfo) (int64, interface{}, error) {
client, err := docker.NewDockerClient()
if err != nil {
return 0, nil, err
}
list, err := client.NetworkList(context.TODO(), types.NetworkListOptions{})
if err != nil {
return 0, nil, err
}
var (
data []dto.Network
records []types.NetworkResource
)
total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
records = make([]types.NetworkResource, 0)
} else {
if end >= total {
end = total
}
records = list[start:end]
}
for _, item := range records {
tag := make([]string, 0)
for key, val := range item.Labels {
tag = append(tag, fmt.Sprintf("%s=%s", key, val))
}
var ipam network.IPAMConfig
if len(item.IPAM.Config) > 0 {
ipam = item.IPAM.Config[0]
}
data = append(data, dto.Network{
ID: item.ID,
CreatedAt: item.Created,
Name: item.Name,
Driver: item.Driver,
IPAMDriver: item.IPAM.Driver,
Subnet: ipam.Subnet,
Gateway: ipam.Gateway,
Attachable: item.Attachable,
Labels: tag,
})
}
return int64(total), data, nil
}
func (u *ContainerService) DeleteNetwork(req dto.BatchDelete) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
for _, id := range req.Ids {
if err := client.NetworkRemove(context.TODO(), id); err != nil {
return err
}
}
return nil
}
func (u *ContainerService) CreateNetwork(req dto.NetworkCreat) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
var (
ipam network.IPAMConfig
hasConf bool
)
if len(req.Subnet) != 0 {
ipam.Subnet = req.Subnet
hasConf = true
}
if len(req.Gateway) != 0 {
ipam.Gateway = req.Gateway
hasConf = true
}
if len(req.IPRange) != 0 {
ipam.IPRange = req.IPRange
hasConf = true
}
options := types.NetworkCreate{
Driver: req.Driver,
Options: stringsToMap(req.Options),
Labels: stringsToMap(req.Labels),
}
if hasConf {
options.IPAM = &network.IPAM{Config: []network.IPAMConfig{ipam}}
}
if _, err := client.NetworkCreate(context.TODO(), req.Name, options); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,98 @@
package service
import (
"context"
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume"
)
func (u *ContainerService) PageVolume(req dto.PageInfo) (int64, interface{}, error) {
client, err := docker.NewDockerClient()
if err != nil {
return 0, nil, err
}
list, err := client.VolumeList(context.TODO(), filters.NewArgs())
if err != nil {
return 0, nil, err
}
var (
data []dto.Volume
records []*types.Volume
)
total, start, end := len(list.Volumes), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
records = make([]*types.Volume, 0)
} else {
if end >= total {
end = total
}
records = list.Volumes[start:end]
}
for _, item := range records {
tag := make([]string, 0)
for _, val := range item.Labels {
tag = append(tag, val)
}
createTime, _ := time.Parse("2006-01-02T15:04:05Z", item.CreatedAt)
data = append(data, dto.Volume{
CreatedAt: createTime,
Name: item.Name,
Driver: item.Driver,
Mountpoint: item.Mountpoint,
Labels: tag,
})
}
return int64(total), data, nil
}
func (u *ContainerService) ListVolume() ([]dto.Options, error) {
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
list, err := client.VolumeList(context.TODO(), filters.NewArgs())
if err != nil {
return nil, err
}
var data []dto.Options
for _, item := range list.Volumes {
data = append(data, dto.Options{
Option: item.Name,
})
}
return data, nil
}
func (u *ContainerService) DeleteVolume(req dto.BatchDelete) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
for _, id := range req.Ids {
if err := client.VolumeRemove(context.TODO(), id, true); err != nil {
return err
}
}
return nil
}
func (u *ContainerService) CreateVolume(req dto.VolumeCreat) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
options := volume.VolumeCreateBody{
Name: req.Name,
Driver: req.Driver,
DriverOpts: stringsToMap(req.Options),
Labels: stringsToMap(req.Labels),
}
if _, err := client.VolumeCreate(context.TODO(), options); err != nil {
return err
}
return nil
}

View File

@ -38,7 +38,6 @@ var (
tagRepo = repo.RepoGroupApp.TagRepo
appInstallRepo = repo.RepoGroupApp.AppInstallRepo
appInstallResourceRepo = repo.RepoGroupApp.AppInstallResourceRpo
appContainerRepo = repo.RepoGroupApp.AppContainerRepo
dataBaseRepo = repo.RepoGroupApp.DatabaseRepo
appInstallBackupRepo = repo.RepoGroupApp.AppInstallBackupRepo
)

View File

@ -11,10 +11,10 @@ import (
"os"
"time"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"github.com/1Panel-dev/1Panel/utils/docker"
"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/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
)

View File

@ -4,8 +4,8 @@ import (
"encoding/json"
"io/ioutil"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
)

View File

@ -1,115 +0,0 @@
package service
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"testing"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/utils/docker"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/pkg/archive"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
func TestImage(t *testing.T) {
file, err := os.OpenFile(("/tmp/nginx.tar"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
fmt.Println(err)
}
defer file.Close()
client, err := docker.NewDockerClient()
if err != nil {
fmt.Println(err)
}
out, err := client.ImageSave(context.TODO(), []string{"nginx:1.14.2"})
fmt.Println(err)
defer out.Close()
if _, err = io.Copy(file, out); err != nil {
fmt.Println(err)
}
}
func TestBuild(t *testing.T) {
client, err := docker.NewDockerClient()
if err != nil {
fmt.Println(err)
}
tar, err := archive.TarWithOptions("/tmp/testbuild/", &archive.TarOptions{})
if err != nil {
fmt.Println(err)
}
opts := types.ImageBuildOptions{
Dockerfile: "Dockerfile",
Tags: []string{"hello/test:v1"},
Remove: true,
}
res, err := client.ImageBuild(context.TODO(), tar, opts)
if err != nil {
fmt.Println(err)
}
defer res.Body.Close()
}
func TestDeam(t *testing.T) {
file, err := ioutil.ReadFile(constant.DaemonJsonDir)
if err != nil {
fmt.Println(err)
}
deamonMap := make(map[string]interface{})
err = json.Unmarshal(file, &deamonMap)
fmt.Println(err)
for k, v := range deamonMap {
fmt.Println(k, v)
}
if _, ok := deamonMap["insecure-registries"]; ok {
if k, v := deamonMap["insecure-registries"].(string); v {
fmt.Println("string ", k)
}
if k, v := deamonMap["insecure-registries"].([]interface{}); v {
fmt.Println("[]string ", k)
k = append(k, "172.16.10.111:8085")
deamonMap["insecure-registries"] = k
}
}
newss, err := json.Marshal(deamonMap)
if err != nil {
fmt.Println(err)
}
fmt.Println(string(newss))
if err := ioutil.WriteFile(constant.DaemonJsonDir, newss, 0777); err != nil {
fmt.Println(err)
}
}
func TestNetwork(t *testing.T) {
client, err := docker.NewDockerClient()
if err != nil {
fmt.Println(err)
}
options := types.ContainerListOptions{All: true}
options.Filters = filters.NewArgs()
options.Filters.Add("label", "maintainer")
ss, _ := client.ContainerList(context.TODO(), options)
fmt.Println(ss)
}
func TestContainer(t *testing.T) {
client, err := docker.NewDockerClient()
if err != nil {
fmt.Println(err)
}
_, err = client.ContainerCreate(context.TODO(), &container.Config{}, &container.HostConfig{}, &network.NetworkingConfig{}, &v1.Platform{}, "test")
if err != nil {
fmt.Println(err)
}
}

View File

@ -1,8 +1,8 @@
package router
import (
v1 "github.com/1Panel-dev/1Panel/app/api/v1"
"github.com/1Panel-dev/1Panel/middleware"
v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1"
"github.com/1Panel-dev/1Panel/backend/middleware"
"github.com/gin-gonic/gin"
)
@ -20,6 +20,8 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
Use(middleware.OperationRecord())
baseApi := v1.ApiGroupApp.BaseApi
{
baRouter.GET("/exec", baseApi.ContainerExec)
baRouter.POST("/search", baseApi.SearchContainer)
baRouter.POST("/inspect", baseApi.Inspect)
baRouter.POST("", baseApi.ContainerCreate)

View File

@ -5,12 +5,12 @@ import (
"fmt"
"testing"
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"github.com/1Panel-dev/1Panel/init/db"
"github.com/1Panel-dev/1Panel/init/log"
"github.com/1Panel-dev/1Panel/init/viper"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/init/db"
"github.com/1Panel-dev/1Panel/backend/init/log"
"github.com/1Panel-dev/1Panel/backend/init/viper"
)
func TestMinio(t *testing.T) {

View File

@ -7,12 +7,12 @@ import (
"os"
"testing"
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"github.com/1Panel-dev/1Panel/init/db"
"github.com/1Panel-dev/1Panel/init/log"
"github.com/1Panel-dev/1Panel/init/viper"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/init/db"
"github.com/1Panel-dev/1Panel/backend/init/log"
"github.com/1Panel-dev/1Panel/backend/init/viper"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)

View File

@ -5,12 +5,12 @@ import (
"fmt"
"testing"
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"github.com/1Panel-dev/1Panel/init/db"
"github.com/1Panel-dev/1Panel/init/log"
"github.com/1Panel-dev/1Panel/init/viper"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/init/db"
"github.com/1Panel-dev/1Panel/backend/init/log"
"github.com/1Panel-dev/1Panel/backend/init/viper"
)
func TestCronS(t *testing.T) {

View File

@ -0,0 +1,102 @@
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) (*ExecWsSession, error) {
_, _ = hijacked.Write([]byte(fmt.Sprintf("stty cols %d rows %d && clear \r", cols, rows)))
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 {
global.LOG.Errorf("reading webSocket message failed, err: %v", err)
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(width int, height int) {
_, _ = sws.conn.Write([]byte(fmt.Sprintf("stty cols %d rows %d && clear \r", width, height)))
}

View File

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

View File

@ -109,6 +109,7 @@ export namespace Container {
gateway: string;
createdAt: string;
attachable: string;
expand: boolean;
}
export interface NetworkCreate {
name: string;
@ -169,6 +170,7 @@ export namespace Container {
export interface ComposeInfo {
name: string;
createdAt: string;
createdBy: string;
containerNumber: number;
configFile: string;
workdir: string;

View File

@ -9,6 +9,7 @@
</template>
<SubItem :menuList="subItem.children" />
</el-sub-menu>
<el-menu-item v-else-if="subItem.children && subItem.children.length === 1" :index="subItem.children[0].path">
<el-icon>
<SvgIcon :iconName="(subItem.meta?.icon as string)" :className="'svg-icon'"></SvgIcon>
@ -17,7 +18,11 @@
<span>{{ $t(subItem.meta?.title as string) }}</span>
</template>
</el-menu-item>
<el-menu-item v-else :index="subItem.path">
<el-icon v-if="subItem.meta?.icon">
<SvgIcon :iconName="(subItem.meta?.icon as string)" :className="'svg-icon'"></SvgIcon>
</el-icon>
<template #title>
<span style="margin-left: 10px">{{ $t(subItem.meta?.title as string) }}</span>
</template>

View File

@ -16,6 +16,7 @@ export default {
cancel: 'Cancel',
reset: 'Reset',
conn: 'Connect',
disconn: 'Disconnect',
clean: 'Clean',
login: 'Login',
close: 'Close',
@ -169,6 +170,9 @@ export default {
lastHour: 'Last Hour',
last10Min: 'Last 10 Minutes',
custom: 'Custom',
containerTerminal: 'Container terminal',
containerCreate: 'Container create',
port: 'Port',
exposePort: 'Expose port',

View File

@ -17,6 +17,7 @@ export default {
cancel: '取消',
reset: '重置',
conn: '连接',
disconn: '断开',
clean: '清空',
login: '登录',
close: '关闭',
@ -167,6 +168,9 @@ export default {
lastHour: '最近 1 小时',
last10Min: '最近 10 分钟',
custom: '自定义',
containerTerminal: '容器终端',
containerCreate: '容器创建',
port: '端口',
exposePort: '暴露端口',

View File

@ -24,7 +24,6 @@ const containerRouter = {
path: 'image',
name: 'Image',
component: () => import('@/views/container/image/index.vue'),
props: true,
hidden: true,
meta: {
activeMenu: '/containers',
@ -34,7 +33,6 @@ const containerRouter = {
path: 'network',
name: 'Network',
component: () => import('@/views/container/network/index.vue'),
props: true,
hidden: true,
meta: {
activeMenu: '/containers',
@ -44,7 +42,6 @@ const containerRouter = {
path: 'volume',
name: 'Volume',
component: () => import('@/views/container/volume/index.vue'),
props: true,
hidden: true,
meta: {
activeMenu: '/containers',
@ -54,7 +51,6 @@ const containerRouter = {
path: 'repo',
name: 'Repo',
component: () => import('@/views/container/repo/index.vue'),
props: true,
hidden: true,
meta: {
activeMenu: '/containers',
@ -64,7 +60,6 @@ const containerRouter = {
path: 'compose',
name: 'Compose',
component: () => import('@/views/container/compose/index.vue'),
props: true,
hidden: true,
meta: {
activeMenu: '/containers',
@ -74,7 +69,6 @@ const containerRouter = {
path: 'template',
name: 'composeTemplate',
component: () => import('@/views/container/template/index.vue'),
props: true,
hidden: true,
meta: {
activeMenu: '/containers',

View File

@ -40,15 +40,16 @@
<el-link @click="goContainer(row.name)" type="primary">{{ row.name }}</el-link>
</template>
</el-table-column>
<el-table-column :label="$t('container.from')" prop="createdBy" min-width="80" fix />
<el-table-column :label="$t('container.containerNumber')" prop="containerNumber" min-width="80" fix />
<el-table-column :label="$t('container.containerNumber')" prop="contaienrs" min-width="80" fix>
<el-table-column :label="$t('container.container')" prop="contaienrs" min-width="80" fix>
<template #default="{ row }">
<div v-for="(item, index) in row.containers" :key="index">
<div v-if="row.expand || (!row.expand && index < 1)">
<div v-if="row.expand || (!row.expand && index < 3)">
<el-tag>{{ item.name }} [{{ item.state }}]</el-tag>
</div>
</div>
<div v-if="!row.expand">
<div v-if="!row.expand && row.containers.length > 3">
<el-button type="primary" link @click="row.expand = true">
{{ $t('commons.button.expand') }}...
</el-button>
@ -91,6 +92,7 @@ const search = async () => {
await searchCompose(params).then((res) => {
if (res.data) {
data.value = res.data.items;
paginationConfig.total = res.data.total;
}
});
};
@ -132,7 +134,6 @@ const onOperate = async (operation: string) => {
});
});
};
// const buttons = [];
onMounted(() => {
search();

View File

@ -96,6 +96,9 @@ const rules = reactive({
const loadTemplates = async () => {
const res = await listComposeTemplate();
templateOptions.value = res.data;
if (templateOptions.value && templateOptions.value.length !== 0) {
form.template = templateOptions.value[0].id;
}
};
const acceptParams = (): void => {
@ -104,7 +107,6 @@ const acceptParams = (): void => {
form.from = 'edit';
form.path = '';
form.file = '';
form.template = 0;
loadTemplates();
};
const emit = defineEmits<{ (e: 'search'): void }>();

View File

@ -159,6 +159,7 @@
</el-dialog>
<CreateDialog @search="search" ref="dialogCreateRef" />
<MonitorDialog ref="dialogMonitorRef" />
<TerminalDialog ref="dialogTerminalRef" />
</div>
</template>
@ -166,6 +167,7 @@
import ComplexTable from '@/components/complex-table/index.vue';
import CreateDialog from '@/views/container/container/create/index.vue';
import MonitorDialog from '@/views/container/container/monitor/index.vue';
import TerminalDialog from '@/views/container/container/terminal/index.vue';
import Submenu from '@/views/container/index.vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
@ -259,6 +261,11 @@ const onMonitor = (containerID: string) => {
dialogMonitorRef.value!.acceptParams({ containerID: containerID });
};
const dialogTerminalRef = ref();
const onTerminal = (containerID: string) => {
dialogTerminalRef.value!.acceptParams({ containerID: containerID });
};
const onInspect = async (id: string) => {
const res = await inspect({ id: id, type: 'container' });
detailInfo.value = JSON.stringify(JSON.parse(res.data), null, 2);
@ -375,8 +382,20 @@ const onOperate = async (operation: string) => {
};
const buttons = [
{
label: i18n.global.t('file.terminal'),
disabled: (row: Container.ContainerInfo) => {
return row.state !== 'running';
},
click: (row: Container.ContainerInfo) => {
onTerminal(row.containerID);
},
},
{
label: i18n.global.t('container.monitor'),
disabled: (row: Container.ContainerInfo) => {
return row.state !== 'running';
},
click: (row: Container.ContainerInfo) => {
onMonitor(row.containerID);
},

View File

@ -0,0 +1,207 @@
<template>
<el-dialog
v-model="terminalVisiable"
:destroy-on-close="true"
@close="onClose"
:close-on-click-modal="false"
width="70%"
>
<template #header>
<div class="card-header">
<span>{{ $t('container.containerTerminal') }}</span>
</div>
</template>
<el-form ref="formRef" :model="form" label-width="80px">
<el-form-item label="User" prop="user" :rules="Rules.requiredInput">
<el-input style="width: 30%" clearable placeholder="root" v-model="form.user" />
</el-form-item>
<el-form-item :label="$t('container.custom')" prop="custom">
<el-switch v-model="form.isCustom" @change="form.command = ''" />
</el-form-item>
<el-form-item v-if="form.isCustom" label="Command" prop="command" :rules="Rules.requiredInput">
<el-input style="width: 30%" clearable v-model="form.command" />
</el-form-item>
<el-form-item v-if="!form.isCustom" label="Command" prop="command" :rules="Rules.requiredSelect">
<el-select style="width: 30%" allow-create filterable clearable v-model="form.command">
<el-option value="/bin/ash" label="/bin/ash" />
<el-option value="/bin/bash" label="/bin/bash" />
<el-option value="/bin/sh" label="/bin/sh" />
</el-select>
</el-form-item>
<el-form-item>
<el-button v-if="!terminalOpen" @click="initTerm(formRef)">{{ $t('commons.button.conn') }}</el-button>
<el-button v-else @click="onClose()">{{ $t('commons.button.disconn') }}</el-button>
</el-form-item>
</el-form>
<div :id="'terminal-exec'"></div>
<template #footer>
<span class="dialog-footer">
<el-button @click="terminalVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { ElForm, FormInstance } from 'element-plus';
import { Terminal } from 'xterm';
import { AttachAddon } from 'xterm-addon-attach';
import { Base64 } from 'js-base64';
import 'xterm/css/xterm.css';
import { FitAddon } from 'xterm-addon-fit';
import { Rules } from '@/global/form-rules';
const terminalVisiable = ref(false);
const terminalOpen = ref(false);
const fitAddon = new FitAddon();
let terminalSocket = ref(null) as unknown as WebSocket;
let term = ref(null) as unknown as Terminal;
const loading = ref(true);
const runRealTerminal = () => {
loading.value = false;
};
const form = reactive({
isCustom: false,
command: '',
user: '',
containerID: '',
});
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
interface DialogProps {
containerID: string;
}
const acceptParams = async (params: DialogProps): Promise<void> => {
terminalVisiable.value = true;
form.containerID = params.containerID;
form.isCustom = false;
form.user = 'root';
form.command = '/bin/bash';
terminalOpen.value = false;
window.addEventListener('resize', changeTerminalSize);
};
const onWSReceive = (message: any) => {
if (!isJson(message.data)) {
return;
}
const data = JSON.parse(message.data);
term.element && term.focus();
term.write(data.Data);
};
function isJson(str: string) {
try {
if (typeof JSON.parse(str) === 'object') {
return true;
}
} catch {
return false;
}
}
const errorRealTerminal = (ex: any) => {
let message = ex.message;
if (!message) message = 'disconnected';
term.write(`\x1b[31m${message}\x1b[m\r\n`);
};
const closeRealTerminal = (ev: CloseEvent) => {
term.write(ev.reason);
};
const initTerm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
terminalOpen.value = true;
let ifm = document.getElementById('terminal-exec') as HTMLInputElement | null;
term = new Terminal({
lineHeight: 1.2,
fontSize: 12,
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
theme: {
background: '#000000',
},
cursorBlink: true,
cursorStyle: 'underline',
scrollback: 100,
tabStopWidth: 4,
});
if (ifm) {
term.open(ifm);
terminalSocket = new WebSocket(
`ws://localhost:9999/api/v1/containers/exec?containerid=${form.containerID}&cols=${term.cols}&rows=${term.rows}&user=${form.user}&command=${form.command}`,
);
terminalSocket.onopen = runRealTerminal;
terminalSocket.onmessage = onWSReceive;
terminalSocket.onclose = closeRealTerminal;
terminalSocket.onerror = errorRealTerminal;
term.onData((data: any) => {
if (isWsOpen()) {
terminalSocket.send(
JSON.stringify({
type: 'cmd',
cmd: Base64.encode(data),
}),
);
}
});
term.loadAddon(new AttachAddon(terminalSocket));
term.loadAddon(fitAddon);
setTimeout(() => {
fitAddon.fit();
if (isWsOpen()) {
terminalSocket.send(
JSON.stringify({
type: 'resize',
cols: term.cols,
rows: term.rows,
}),
);
}
}, 30);
}
});
};
const fitTerm = () => {
fitAddon.fit();
};
const isWsOpen = () => {
const readyState = terminalSocket && terminalSocket.readyState;
if (readyState) {
return readyState === 1;
}
return false;
};
function onClose() {
terminalOpen.value = false;
window.removeEventListener('resize', changeTerminalSize);
if (isWsOpen()) {
terminalSocket && terminalSocket.close();
term.dispose();
}
}
function changeTerminalSize() {
fitTerm();
const { cols, rows } = term;
if (isWsOpen()) {
terminalSocket.send(
JSON.stringify({
type: 'resize',
cols: cols,
rows: rows,
}),
);
}
}
defineExpose({
acceptParams,
});
</script>

View File

@ -54,7 +54,7 @@ const props = withDefaults(defineProps<MenuProps>(), {
activeName: 'container',
});
const active = ref();
const active = ref('container');
onMounted(() => {
if (props.activeName) {

View File

@ -28,10 +28,15 @@
<el-table-column :label="$t('container.gateway')" min-width="80" prop="gateway" fix />
<el-table-column :label="$t('container.tag')" min-width="140" fix>
<template #default="{ row }">
<div v-for="(item, index) of row.labels" :key="index">
<el-tooltip class="item" :content="item" placement="top">
<div v-for="(item, index) in row.labels" :key="index">
<div v-if="row.expand || (!row.expand && index < 3)">
<el-tag>{{ item }}</el-tag>
</el-tooltip>
</div>
</div>
<div v-if="!row.expand && row.labels.length > 3">
<el-button type="primary" link @click="row.expand = true">
{{ $t('commons.button.expand') }}...
</el-button>
</div>
</template>
</el-table-column>

1
go.mod
View File

@ -148,5 +148,4 @@ require (
golang.org/x/tools v0.1.12 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect
gotest.tools/v3 v3.3.0 // indirect
)

40
go.sum
View File

@ -126,12 +126,9 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
<<<<<<< HEAD:go.sum
=======
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
>>>>>>> 94a2b8a (feat: 实现容器网络功能):backend/go.sum
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -147,12 +144,10 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
<<<<<<< HEAD:go.sum
github.com/compose-spec/compose-go v1.6.0 h1:7Ol/UULMUtbPmB0EYrETASRoum821JpOh/XaEf+hN+Q=
github.com/compose-spec/compose-go v1.6.0/go.mod h1:os+Ulh2jlZxY1XT1hbciERadjSUU/BtZ6+gcN7vD7J0=
=======
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/compose-spec/compose-go v1.6.0 h1:7Ol/UULMUtbPmB0EYrETASRoum821JpOh/XaEf+hN+Q=
github.com/compose-spec/compose-go v1.6.0/go.mod h1:os+Ulh2jlZxY1XT1hbciERadjSUU/BtZ6+gcN7vD7J0=
github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
@ -245,7 +240,6 @@ github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/
github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4=
github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
>>>>>>> 94a2b8a (feat: 实现容器网络功能):backend/go.sum
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
@ -286,17 +280,14 @@ github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
<<<<<<< HEAD:go.sum
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/distribution/distribution/v3 v3.0.0-20220725133111-4bf3547399eb h1:oCCuuU3kMO3sjZH/p7LamvQNW9SWoT4yQuMGcdSxGAE=
github.com/distribution/distribution/v3 v3.0.0-20220725133111-4bf3547399eb/go.mod h1:28YO/VJk9/64+sTGNuYaBjWxrXTPrj0C0XmgTIOjxX4=
=======
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
>>>>>>> 94a2b8a (feat: 实现容器网络功能):backend/go.sum
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
@ -556,16 +547,13 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
<<<<<<< HEAD:go.sum
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
=======
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
>>>>>>> 94a2b8a (feat: 实现容器网络功能):backend/go.sum
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
@ -665,13 +653,10 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
<<<<<<< HEAD:go.sum
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
=======
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
>>>>>>> 94a2b8a (feat: 实现容器网络功能):backend/go.sum
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@ -952,12 +937,6 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
@ -966,9 +945,13 @@ github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlzd/gotp v0.0.0-20220817083547-a63b9d03d72f h1:C8De+7emQKojPBC+mXA0fr39XN5mKjRm9IUzdxI4whI=
github.com/xlzd/gotp v0.0.0-20220817083547-a63b9d03d72f/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
@ -1487,7 +1470,6 @@ gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo=
gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=