mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-25 06:56:32 +08:00
feat: 增加容器编辑功能 (#1381)
This commit is contained in:
parent
b7cda1d2f1
commit
352978b54d
13 changed files with 612 additions and 165 deletions
|
|
@ -153,17 +153,69 @@ func (b *BaseApi) OperatorCompose(c *gin.Context) {
|
||||||
helper.SuccessWithData(c, nil)
|
helper.SuccessWithData(c, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags Container
|
||||||
|
// @Summary Update container
|
||||||
|
// @Description 更新容器
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.ContainerOperate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /containers/update [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["name","image"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"更新容器 [name][image]","formatEN":"update container [name][image]"}
|
||||||
|
func (b *BaseApi) ContainerUpdate(c *gin.Context) {
|
||||||
|
var req dto.ContainerOperate
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := global.VALID.Struct(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := containerService.ContainerUpdate(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Container
|
||||||
|
// @Summary Load container info
|
||||||
|
// @Description 获取容器表单信息
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.OperationWithName true "request"
|
||||||
|
// @Success 200 {object} dto.ContainerOperate
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /containers/info [post]
|
||||||
|
func (b *BaseApi) ContainerInfo(c *gin.Context) {
|
||||||
|
var req dto.OperationWithName
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := global.VALID.Struct(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := containerService.ContainerInfo(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags Container
|
// @Tags Container
|
||||||
// @Summary Create container
|
// @Summary Create container
|
||||||
// @Description 创建容器
|
// @Description 创建容器
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.ContainerCreate true "request"
|
// @Param request body dto.ContainerOperate true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Router /containers [post]
|
// @Router /containers [post]
|
||||||
// @x-panel-log {"bodyKeys":["name","image"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"创建容器 [name][image]","formatEN":"create container [name][image]"}
|
// @x-panel-log {"bodyKeys":["name","image"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"创建容器 [name][image]","formatEN":"create container [name][image]"}
|
||||||
func (b *BaseApi) ContainerCreate(c *gin.Context) {
|
func (b *BaseApi) ContainerCreate(c *gin.Context) {
|
||||||
var req dto.ContainerCreate
|
var req dto.ContainerOperate
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ type ContainerInfo struct {
|
||||||
IsFromCompose bool `json:"isFromCompose"`
|
IsFromCompose bool `json:"isFromCompose"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContainerCreate struct {
|
type ContainerOperate struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
PublishAllPorts bool `json:"publishAllPorts"`
|
PublishAllPorts bool `json:"publishAllPorts"`
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,9 @@ type IContainerService interface {
|
||||||
PageCompose(req dto.SearchWithPage) (int64, interface{}, error)
|
PageCompose(req dto.SearchWithPage) (int64, interface{}, error)
|
||||||
CreateCompose(req dto.ComposeCreate) (string, error)
|
CreateCompose(req dto.ComposeCreate) (string, error)
|
||||||
ComposeOperation(req dto.ComposeOperation) error
|
ComposeOperation(req dto.ComposeOperation) error
|
||||||
ContainerCreate(req dto.ContainerCreate) error
|
ContainerCreate(req dto.ContainerOperate) error
|
||||||
|
ContainerUpdate(req dto.ContainerOperate) error
|
||||||
|
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
|
||||||
ContainerLogClean(req dto.OperationWithName) error
|
ContainerLogClean(req dto.OperationWithName) error
|
||||||
ContainerOperation(req dto.ContainerOperation) error
|
ContainerOperation(req dto.ContainerOperation) error
|
||||||
ContainerLogs(wsConn *websocket.Conn, container, since, tail string, follow bool) error
|
ContainerLogs(wsConn *websocket.Conn, container, since, tail string, follow bool) error
|
||||||
|
|
@ -213,55 +215,16 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) (dto.ContainerPruneRepo
|
||||||
return report, nil
|
return report, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ContainerCreate(req dto.ContainerCreate) error {
|
func (u *ContainerService) ContainerCreate(req dto.ContainerOperate) error {
|
||||||
portMap, err := checkPortStats(req.ExposedPorts)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
exposeds := make(nat.PortSet)
|
var config *container.Config
|
||||||
for port := range portMap {
|
var hostConf *container.HostConfig
|
||||||
exposeds[port] = struct{}{}
|
if err := loadConfigInfo(req, config, hostConf); err != nil {
|
||||||
}
|
return err
|
||||||
config := &container.Config{
|
|
||||||
Image: req.Image,
|
|
||||||
Cmd: req.Cmd,
|
|
||||||
Env: req.Env,
|
|
||||||
Labels: stringsToMap(req.Labels),
|
|
||||||
Tty: true,
|
|
||||||
OpenStdin: true,
|
|
||||||
ExposedPorts: exposeds,
|
|
||||||
}
|
|
||||||
hostConf := &container.HostConfig{
|
|
||||||
AutoRemove: req.AutoRemove,
|
|
||||||
PublishAllPorts: req.PublishAllPorts,
|
|
||||||
RestartPolicy: container.RestartPolicy{Name: req.RestartPolicy},
|
|
||||||
}
|
|
||||||
if req.RestartPolicy == "on-failure" {
|
|
||||||
hostConf.RestartPolicy.MaximumRetryCount = 5
|
|
||||||
}
|
|
||||||
if req.NanoCPUs != 0 {
|
|
||||||
hostConf.NanoCPUs = req.NanoCPUs * 1000000000
|
|
||||||
}
|
|
||||||
if req.CPUShares != 0 {
|
|
||||||
hostConf.CPUShares = req.CPUShares
|
|
||||||
}
|
|
||||||
if req.Memory != 0 {
|
|
||||||
hostConf.Memory = req.Memory
|
|
||||||
}
|
|
||||||
if len(req.ExposedPorts) != 0 {
|
|
||||||
hostConf.PortBindings = portMap
|
|
||||||
}
|
|
||||||
if len(req.Volumes) != 0 {
|
|
||||||
config.Volumes = make(map[string]struct{})
|
|
||||||
for _, volume := range req.Volumes {
|
|
||||||
config.Volumes[volume.ContainerDir] = struct{}{}
|
|
||||||
hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", volume.SourceDir, volume.ContainerDir, volume.Mode))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
global.LOG.Infof("new container info %s has been made, now start to create", req.Name)
|
global.LOG.Infof("new container info %s has been made, now start to create", req.Name)
|
||||||
|
|
@ -285,6 +248,95 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerCreate) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error) {
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
oldContainer, err := client.ContainerInspect(ctx, req.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var data dto.ContainerOperate
|
||||||
|
data.Name = strings.ReplaceAll(oldContainer.Name, "/", "")
|
||||||
|
data.Image = oldContainer.Config.Image
|
||||||
|
data.Cmd = oldContainer.Config.Cmd
|
||||||
|
data.Env = oldContainer.Config.Env
|
||||||
|
for key, val := range oldContainer.Config.Labels {
|
||||||
|
data.Labels = append(data.Labels, fmt.Sprintf("%s=%s", key, val))
|
||||||
|
}
|
||||||
|
for key, val := range oldContainer.HostConfig.PortBindings {
|
||||||
|
var itemPort dto.PortHelper
|
||||||
|
if !strings.Contains(string(key), "/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
itemPort.ContainerPort = strings.Split(string(key), "/")[0]
|
||||||
|
itemPort.Protocol = strings.Split(string(key), "/")[1]
|
||||||
|
for _, binds := range val {
|
||||||
|
itemPort.HostIP = binds.HostIP
|
||||||
|
itemPort.HostPort = binds.HostPort
|
||||||
|
data.ExposedPorts = append(data.ExposedPorts, itemPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.AutoRemove = oldContainer.HostConfig.AutoRemove
|
||||||
|
data.PublishAllPorts = oldContainer.HostConfig.PublishAllPorts
|
||||||
|
data.RestartPolicy = oldContainer.HostConfig.RestartPolicy.Name
|
||||||
|
if oldContainer.HostConfig.NanoCPUs != 0 {
|
||||||
|
data.NanoCPUs = oldContainer.HostConfig.NanoCPUs / 1000000000
|
||||||
|
}
|
||||||
|
if oldContainer.HostConfig.Memory != 0 {
|
||||||
|
data.Memory = oldContainer.HostConfig.Memory
|
||||||
|
}
|
||||||
|
for _, bind := range oldContainer.HostConfig.Binds {
|
||||||
|
parts := strings.Split(bind, ":")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data.Volumes = append(data.Volumes, dto.VolumeHelper{SourceDir: parts[0], ContainerDir: parts[1], Mode: parts[2]})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
oldContainer, err := client.ContainerInspect(ctx, req.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !checkImageExist(client, req.Image) {
|
||||||
|
if err := pullImages(ctx, client, req.Image); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config := oldContainer.Config
|
||||||
|
hostConf := oldContainer.HostConfig
|
||||||
|
if err := loadConfigInfo(req, config, hostConf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := client.ContainerRemove(ctx, req.Name, types.ContainerRemoveOptions{Force: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
global.LOG.Infof("new container info %s has been update, now start to recreate", req.Name)
|
||||||
|
container, err := client.ContainerCreate(ctx, config, hostConf, &network.NetworkingConfig{}, &v1.Platform{}, req.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("recreate contianer failed, err: %v", err)
|
||||||
|
}
|
||||||
|
global.LOG.Infof("update container %s successful! now check if the container is started.", req.Name)
|
||||||
|
if err := client.ContainerStart(ctx, container.ID, types.ContainerStartOptions{}); err != nil {
|
||||||
|
return fmt.Errorf("update successful but start failed, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error {
|
func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error {
|
||||||
var err error
|
var err error
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
@ -559,3 +611,44 @@ func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) {
|
||||||
}
|
}
|
||||||
return portMap, nil
|
return portMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadConfigInfo(req dto.ContainerOperate, config *container.Config, hostConf *container.HostConfig) error {
|
||||||
|
portMap, err := checkPortStats(req.ExposedPorts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
exposeds := make(nat.PortSet)
|
||||||
|
for port := range portMap {
|
||||||
|
exposeds[port] = struct{}{}
|
||||||
|
}
|
||||||
|
config.Image = req.Image
|
||||||
|
config.Cmd = req.Cmd
|
||||||
|
config.Env = req.Env
|
||||||
|
config.Labels = stringsToMap(req.Labels)
|
||||||
|
config.ExposedPorts = exposeds
|
||||||
|
|
||||||
|
hostConf.AutoRemove = req.AutoRemove
|
||||||
|
hostConf.PublishAllPorts = req.PublishAllPorts
|
||||||
|
hostConf.RestartPolicy = container.RestartPolicy{Name: req.RestartPolicy}
|
||||||
|
if req.RestartPolicy == "on-failure" {
|
||||||
|
hostConf.RestartPolicy.MaximumRetryCount = 5
|
||||||
|
}
|
||||||
|
if req.NanoCPUs != 0 {
|
||||||
|
hostConf.NanoCPUs = req.NanoCPUs * 1000000000
|
||||||
|
}
|
||||||
|
if req.Memory != 0 {
|
||||||
|
hostConf.Memory = req.Memory
|
||||||
|
}
|
||||||
|
if len(req.ExposedPorts) != 0 {
|
||||||
|
hostConf.PortBindings = portMap
|
||||||
|
}
|
||||||
|
hostConf.Binds = []string{}
|
||||||
|
if len(req.Volumes) != 0 {
|
||||||
|
config.Volumes = make(map[string]struct{})
|
||||||
|
for _, volume := range req.Volumes {
|
||||||
|
config.Volumes[volume.ContainerDir] = struct{}{}
|
||||||
|
hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", volume.SourceDir, volume.ContainerDir, volume.Mode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
|
||||||
baRouter.GET("/stats/:id", baseApi.ContainerStats)
|
baRouter.GET("/stats/:id", baseApi.ContainerStats)
|
||||||
|
|
||||||
baRouter.POST("", baseApi.ContainerCreate)
|
baRouter.POST("", baseApi.ContainerCreate)
|
||||||
|
baRouter.POST("/update", baseApi.ContainerUpdate)
|
||||||
|
baRouter.POST("/info", baseApi.ContainerInfo)
|
||||||
baRouter.POST("/search", baseApi.SearchContainer)
|
baRouter.POST("/search", baseApi.SearchContainer)
|
||||||
baRouter.GET("/search/log", baseApi.ContainerLogs)
|
baRouter.GET("/search/log", baseApi.ContainerLogs)
|
||||||
baRouter.POST("/clean/log", baseApi.CleanContainerLog)
|
baRouter.POST("/clean/log", baseApi.CleanContainerLog)
|
||||||
|
|
|
||||||
|
|
@ -916,7 +916,7 @@ var doc = `{
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/dto.ContainerCreate"
|
"$ref": "#/definitions/dto.ContainerOperate"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -1781,6 +1781,42 @@ var doc = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/containers/info": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "获取容器表单信息",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Container"
|
||||||
|
],
|
||||||
|
"summary": "Load container info",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.OperationWithName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ContainerOperate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/containers/inspect": {
|
"/containers/inspect": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -2597,6 +2633,49 @@ var doc = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/containers/update": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "更新容器",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Container"
|
||||||
|
],
|
||||||
|
"summary": "Update container",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ContainerOperate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-panel-log": {
|
||||||
|
"BeforeFuntions": [],
|
||||||
|
"bodyKeys": [
|
||||||
|
"name",
|
||||||
|
"image"
|
||||||
|
],
|
||||||
|
"formatEN": "update container [name][image]",
|
||||||
|
"formatZH": "更新容器 [name][image]",
|
||||||
|
"paramKeys": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/containers/volume": {
|
"/containers/volume": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -10584,7 +10663,7 @@ var doc = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dto.ContainerCreate": {
|
"dto.ContainerOperate": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"autoRemove": {
|
"autoRemove": {
|
||||||
|
|
@ -10596,6 +10675,9 @@ var doc = `{
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cpushares": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
|
|
||||||
|
|
@ -902,7 +902,7 @@
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/dto.ContainerCreate"
|
"$ref": "#/definitions/dto.ContainerOperate"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
@ -1767,6 +1767,42 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/containers/info": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "获取容器表单信息",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Container"
|
||||||
|
],
|
||||||
|
"summary": "Load container info",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.OperationWithName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ContainerOperate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/containers/inspect": {
|
"/containers/inspect": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -2583,6 +2619,49 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/containers/update": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "更新容器",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Container"
|
||||||
|
],
|
||||||
|
"summary": "Update container",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ContainerOperate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-panel-log": {
|
||||||
|
"BeforeFuntions": [],
|
||||||
|
"bodyKeys": [
|
||||||
|
"name",
|
||||||
|
"image"
|
||||||
|
],
|
||||||
|
"formatEN": "update container [name][image]",
|
||||||
|
"formatZH": "更新容器 [name][image]",
|
||||||
|
"paramKeys": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/containers/volume": {
|
"/containers/volume": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -10570,7 +10649,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dto.ContainerCreate": {
|
"dto.ContainerOperate": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"autoRemove": {
|
"autoRemove": {
|
||||||
|
|
@ -10582,6 +10661,9 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"cpushares": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,7 @@ definitions:
|
||||||
- name
|
- name
|
||||||
- path
|
- path
|
||||||
type: object
|
type: object
|
||||||
dto.ContainerCreate:
|
dto.ContainerOperate:
|
||||||
properties:
|
properties:
|
||||||
autoRemove:
|
autoRemove:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
|
@ -261,6 +261,8 @@ definitions:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
cpushares:
|
||||||
|
type: integer
|
||||||
env:
|
env:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -3926,7 +3928,7 @@ paths:
|
||||||
name: request
|
name: request
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/dto.ContainerCreate'
|
$ref: '#/definitions/dto.ContainerOperate'
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: ""
|
description: ""
|
||||||
|
|
@ -4483,6 +4485,28 @@ paths:
|
||||||
formatEN: tag image [reponame][targetName]
|
formatEN: tag image [reponame][targetName]
|
||||||
formatZH: tag 镜像 [reponame][targetName]
|
formatZH: tag 镜像 [reponame][targetName]
|
||||||
paramKeys: []
|
paramKeys: []
|
||||||
|
/containers/info:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: 获取容器表单信息
|
||||||
|
parameters:
|
||||||
|
- description: request
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.OperationWithName'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ContainerOperate'
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Load container info
|
||||||
|
tags:
|
||||||
|
- Container
|
||||||
/containers/inspect:
|
/containers/inspect:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
@ -5000,6 +5024,34 @@ paths:
|
||||||
formatEN: update compose template information [name]
|
formatEN: update compose template information [name]
|
||||||
formatZH: 更新 compose 模版 [name]
|
formatZH: 更新 compose 模版 [name]
|
||||||
paramKeys: []
|
paramKeys: []
|
||||||
|
/containers/update:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: 更新容器
|
||||||
|
parameters:
|
||||||
|
- description: request
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ContainerOperate'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: ""
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Update container
|
||||||
|
tags:
|
||||||
|
- Container
|
||||||
|
x-panel-log:
|
||||||
|
BeforeFuntions: []
|
||||||
|
bodyKeys:
|
||||||
|
- name
|
||||||
|
- image
|
||||||
|
formatEN: update container [name][image]
|
||||||
|
formatZH: 更新容器 [name][image]
|
||||||
|
paramKeys: []
|
||||||
/containers/volume:
|
/containers/volume:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,18 @@ export namespace Container {
|
||||||
name: string;
|
name: string;
|
||||||
filters: string;
|
filters: string;
|
||||||
}
|
}
|
||||||
export interface ContainerCreate {
|
export interface ContainerHelper {
|
||||||
name: string;
|
name: string;
|
||||||
image: string;
|
image: string;
|
||||||
|
cmdStr: string;
|
||||||
|
memoryUnit: string;
|
||||||
|
memoryItem: number;
|
||||||
cmd: Array<string>;
|
cmd: Array<string>;
|
||||||
publishAllPorts: boolean;
|
publishAllPorts: boolean;
|
||||||
exposedPorts: Array<Port>;
|
exposedPorts: Array<Port>;
|
||||||
nanoCPUs: number;
|
nanoCPUs: number;
|
||||||
|
cpuShares: number;
|
||||||
|
cpuUnit: string;
|
||||||
memory: number;
|
memory: number;
|
||||||
volumes: Array<Volume>;
|
volumes: Array<Volume>;
|
||||||
autoRemove: boolean;
|
autoRemove: boolean;
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,15 @@ import { Container } from '../interface/container';
|
||||||
export const searchContainer = (params: Container.ContainerSearch) => {
|
export const searchContainer = (params: Container.ContainerSearch) => {
|
||||||
return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params, 400000);
|
return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params, 400000);
|
||||||
};
|
};
|
||||||
export const createContainer = (params: Container.ContainerCreate) => {
|
export const createContainer = (params: Container.ContainerHelper) => {
|
||||||
return http.post(`/containers`, params, 3000000);
|
return http.post(`/containers`, params, 3000000);
|
||||||
};
|
};
|
||||||
|
export const updateContainer = (params: Container.ContainerHelper) => {
|
||||||
|
return http.post(`/containers/update`, params, 3000000);
|
||||||
|
};
|
||||||
|
export const loadContainerInfo = (name: string) => {
|
||||||
|
return http.post<Container.ContainerHelper>(`/containers/info`, { name: name });
|
||||||
|
};
|
||||||
export const cleanContainerLog = (containerName: string) => {
|
export const cleanContainerLog = (containerName: string) => {
|
||||||
return http.post(`/containers/clean/log`, { name: containerName });
|
return http.post(`/containers/clean/log`, { name: containerName });
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -444,6 +444,8 @@ const message = {
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
createContainer: 'Create container',
|
createContainer: 'Create container',
|
||||||
|
updateContaienrHelper:
|
||||||
|
'Container editing requires rebuilding the container. Any data that has not been persisted will be lost. Do you want to continue?',
|
||||||
containerList: 'Container list',
|
containerList: 'Container list',
|
||||||
operatorHelper: '{0} will be performed on the selected container. Do you want to continue?',
|
operatorHelper: '{0} will be performed on the selected container. Do you want to continue?',
|
||||||
operatorAppHelper:
|
operatorAppHelper:
|
||||||
|
|
@ -489,6 +491,7 @@ const message = {
|
||||||
|
|
||||||
user: 'User',
|
user: 'User',
|
||||||
command: 'Command',
|
command: 'Command',
|
||||||
|
commandHelper: 'Please enter the correct command, separated by spaces if there are multiple commands.',
|
||||||
custom: 'Custom',
|
custom: 'Custom',
|
||||||
emptyUser: 'When empty, you will log in as default',
|
emptyUser: 'When empty, you will log in as default',
|
||||||
containerTerminal: 'Terminal',
|
containerTerminal: 'Terminal',
|
||||||
|
|
@ -514,9 +517,10 @@ const message = {
|
||||||
mode: 'Mode',
|
mode: 'Mode',
|
||||||
env: 'Environment',
|
env: 'Environment',
|
||||||
restartPolicy: 'Restart policy',
|
restartPolicy: 'Restart policy',
|
||||||
|
always: 'always',
|
||||||
unlessStopped: 'unless-stopped',
|
unlessStopped: 'unless-stopped',
|
||||||
onFailure: 'on-failure(five times by default)',
|
onFailure: 'on-failure(five times by default)',
|
||||||
no: 'no',
|
no: 'never',
|
||||||
|
|
||||||
image: 'Image',
|
image: 'Image',
|
||||||
imagePull: 'Image pull',
|
imagePull: 'Image pull',
|
||||||
|
|
|
||||||
|
|
@ -453,6 +453,7 @@ const message = {
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
createContainer: '创建容器',
|
createContainer: '创建容器',
|
||||||
|
updateContaienrHelper: '容器编辑需要重建容器,任何未持久化的数据将会丢失,是否继续?',
|
||||||
containerList: '容器列表',
|
containerList: '容器列表',
|
||||||
operatorHelper: '将对选中容器进行 {0} 操作,是否继续?',
|
operatorHelper: '将对选中容器进行 {0} 操作,是否继续?',
|
||||||
operatorAppHelper: '存在来源于应用商店的容器,{0} 操作可能会影响到该服务的正常使用,是否确认?',
|
operatorAppHelper: '存在来源于应用商店的容器,{0} 操作可能会影响到该服务的正常使用,是否确认?',
|
||||||
|
|
@ -495,6 +496,7 @@ const message = {
|
||||||
|
|
||||||
user: '用户',
|
user: '用户',
|
||||||
command: '命令',
|
command: '命令',
|
||||||
|
commandHelper: '请输入正确的命令,多个命令空格分割',
|
||||||
custom: '自定义',
|
custom: '自定义',
|
||||||
containerTerminal: '终端',
|
containerTerminal: '终端',
|
||||||
emptyUser: '为空时,将使用容器默认的用户登录',
|
emptyUser: '为空时,将使用容器默认的用户登录',
|
||||||
|
|
@ -520,6 +522,7 @@ const message = {
|
||||||
mode: '权限',
|
mode: '权限',
|
||||||
env: '环境变量',
|
env: '环境变量',
|
||||||
restartPolicy: '重启规则',
|
restartPolicy: '重启规则',
|
||||||
|
always: '一直重启',
|
||||||
unlessStopped: '关闭后重启',
|
unlessStopped: '关闭后重启',
|
||||||
onFailure: '失败后重启(默认重启 5 次)',
|
onFailure: '失败后重启(默认重启 5 次)',
|
||||||
no: '不重启',
|
no: '不重启',
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16">
|
<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16">
|
||||||
<el-button type="primary" @click="onCreate()">
|
<el-button type="primary" @click="onOpenDialog('create')">
|
||||||
{{ $t('container.createContainer') }}
|
{{ $t('container.createContainer') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="primary" plain @click="onClean()">
|
<el-button type="primary" plain @click="onClean()">
|
||||||
|
|
@ -123,7 +123,7 @@
|
||||||
|
|
||||||
<ReNameDialog @search="search" ref="dialogReNameRef" />
|
<ReNameDialog @search="search" ref="dialogReNameRef" />
|
||||||
<ContainerLogDialog ref="dialogContainerLogRef" />
|
<ContainerLogDialog ref="dialogContainerLogRef" />
|
||||||
<CreateDialog @search="search" ref="dialogCreateRef" />
|
<CreateDialog @search="search" ref="dialogOperateRef" />
|
||||||
<MonitorDialog ref="dialogMonitorRef" />
|
<MonitorDialog ref="dialogMonitorRef" />
|
||||||
<TerminalDialog ref="dialogTerminalRef" />
|
<TerminalDialog ref="dialogTerminalRef" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -133,14 +133,21 @@
|
||||||
import Tooltip from '@/components/tooltip/index.vue';
|
import Tooltip from '@/components/tooltip/index.vue';
|
||||||
import TableSetting from '@/components/table-setting/index.vue';
|
import TableSetting from '@/components/table-setting/index.vue';
|
||||||
import ReNameDialog from '@/views/container/container/rename/index.vue';
|
import ReNameDialog from '@/views/container/container/rename/index.vue';
|
||||||
import CreateDialog from '@/views/container/container/create/index.vue';
|
import CreateDialog from '@/views/container/container/operate/index.vue';
|
||||||
import MonitorDialog from '@/views/container/container/monitor/index.vue';
|
import MonitorDialog from '@/views/container/container/monitor/index.vue';
|
||||||
import ContainerLogDialog from '@/views/container/container/log/index.vue';
|
import ContainerLogDialog from '@/views/container/container/log/index.vue';
|
||||||
import TerminalDialog from '@/views/container/container/terminal/index.vue';
|
import TerminalDialog from '@/views/container/container/terminal/index.vue';
|
||||||
import CodemirrorDialog from '@/components/codemirror-dialog/index.vue';
|
import CodemirrorDialog from '@/components/codemirror-dialog/index.vue';
|
||||||
import Status from '@/components/status/index.vue';
|
import Status from '@/components/status/index.vue';
|
||||||
import { reactive, onMounted, ref } from 'vue';
|
import { reactive, onMounted, ref } from 'vue';
|
||||||
import { ContainerOperator, containerPrune, inspect, loadDockerStatus, searchContainer } from '@/api/modules/container';
|
import {
|
||||||
|
ContainerOperator,
|
||||||
|
containerPrune,
|
||||||
|
inspect,
|
||||||
|
loadContainerInfo,
|
||||||
|
loadDockerStatus,
|
||||||
|
searchContainer,
|
||||||
|
} from '@/api/modules/container';
|
||||||
import { Container } from '@/api/interface/container';
|
import { Container } from '@/api/interface/container';
|
||||||
import { ElMessageBox } from 'element-plus';
|
import { ElMessageBox } from 'element-plus';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
|
|
@ -211,9 +218,35 @@ const search = async () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const dialogCreateRef = ref();
|
const dialogOperateRef = ref();
|
||||||
const onCreate = () => {
|
const onEdit = async (container: string) => {
|
||||||
dialogCreateRef.value!.acceptParams();
|
const res = await loadContainerInfo(container);
|
||||||
|
if (res.data) {
|
||||||
|
onOpenDialog('edit', res.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onOpenDialog = async (
|
||||||
|
title: string,
|
||||||
|
rowData: Partial<Container.ContainerHelper> = {
|
||||||
|
cmd: [],
|
||||||
|
cmdStr: '',
|
||||||
|
exposedPorts: [],
|
||||||
|
nanoCPUs: 0,
|
||||||
|
memory: 0,
|
||||||
|
memoryItem: 0,
|
||||||
|
memoryUnit: 'MB',
|
||||||
|
cpuUnit: 'Core',
|
||||||
|
volumes: [],
|
||||||
|
labels: [],
|
||||||
|
env: [],
|
||||||
|
restartPolicy: 'no',
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
let params = {
|
||||||
|
title,
|
||||||
|
rowData: { ...rowData },
|
||||||
|
};
|
||||||
|
dialogOperateRef.value!.acceptParams(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dialogMonitorRef = ref();
|
const dialogMonitorRef = ref();
|
||||||
|
|
@ -336,6 +369,12 @@ const onOperate = async (operation: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttons = [
|
const buttons = [
|
||||||
|
{
|
||||||
|
label: i18n.global.t('commons.button.edit'),
|
||||||
|
click: (row: Container.ContainerInfo) => {
|
||||||
|
onEdit(row.containerID);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: i18n.global.t('file.terminal'),
|
label: i18n.global.t('file.terminal'),
|
||||||
disabled: (row: Container.ContainerInfo) => {
|
disabled: (row: Container.ContainerInfo) => {
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,21 @@
|
||||||
<template #header>
|
<template #header>
|
||||||
<DrawerHeader :header="$t('container.createContainer')" :back="handleClose" />
|
<DrawerHeader :header="$t('container.createContainer')" :back="handleClose" />
|
||||||
</template>
|
</template>
|
||||||
<el-form ref="formRef" label-position="top" v-loading="loading" :model="form" :rules="rules" label-width="80px">
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
label-position="top"
|
||||||
|
v-loading="loading"
|
||||||
|
:model="dialogData.rowData!"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="80px"
|
||||||
|
>
|
||||||
<el-row type="flex" justify="center">
|
<el-row type="flex" justify="center">
|
||||||
<el-col :span="22">
|
<el-col :span="22">
|
||||||
<el-form-item :label="$t('container.name')" prop="name">
|
<el-form-item :label="$t('container.name')" prop="name">
|
||||||
<el-input clearable v-model.trim="form.name" />
|
<el-input clearable v-model.trim="dialogData.rowData!.name" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('container.image')" prop="image">
|
<el-form-item :label="$t('container.image')" prop="image">
|
||||||
<el-select class="widthClass" allow-create filterable v-model="form.image">
|
<el-select class="widthClass" allow-create filterable v-model="dialogData.rowData!.image">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="(item, index) of images"
|
v-for="(item, index) of images"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
|
@ -20,15 +27,15 @@
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('container.port')">
|
<el-form-item :label="$t('container.port')">
|
||||||
<el-radio-group v-model="form.publishAllPorts" class="ml-4">
|
<el-radio-group v-model="dialogData.rowData!.publishAllPorts" class="ml-4">
|
||||||
<el-radio :label="false">{{ $t('container.exposePort') }}</el-radio>
|
<el-radio :label="false">{{ $t('container.exposePort') }}</el-radio>
|
||||||
<el-radio :label="true">{{ $t('container.exposeAll') }}</el-radio>
|
<el-radio :label="true">{{ $t('container.exposeAll') }}</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="!form.publishAllPorts">
|
<el-form-item v-if="!dialogData.rowData!.publishAllPorts">
|
||||||
<el-card class="widthClass">
|
<el-card class="widthClass">
|
||||||
<table style="width: 100%" class="tab-table">
|
<table style="width: 100%" class="tab-table">
|
||||||
<tr v-if="form.exposedPorts.length !== 0">
|
<tr v-if="dialogData.rowData!.exposedPorts.length !== 0">
|
||||||
<th scope="col" width="45%" align="left">
|
<th scope="col" width="45%" align="left">
|
||||||
<label>{{ $t('container.server') }}</label>
|
<label>{{ $t('container.server') }}</label>
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -40,7 +47,7 @@
|
||||||
</th>
|
</th>
|
||||||
<th align="left"></th>
|
<th align="left"></th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="(row, index) in form.exposedPorts" :key="index">
|
<tr v-for="(row, index) in dialogData.rowData!.exposedPorts" :key="index">
|
||||||
<td width="45%">
|
<td width="45%">
|
||||||
<el-input
|
<el-input
|
||||||
:placeholder="$t('container.serverExample')"
|
:placeholder="$t('container.serverExample')"
|
||||||
|
|
@ -76,19 +83,21 @@
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('container.cmd')" prop="cmdStr">
|
<el-form-item :label="$t('container.cmd')" prop="cmdStr">
|
||||||
<el-input :placeholder="$t('container.cmdHelper')" v-model="form.cmdStr" />
|
<el-input :placeholder="$t('container.cmdHelper')" v-model="dialogData.rowData!.cmdStr" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="autoRemove">
|
<el-form-item prop="autoRemove">
|
||||||
<el-checkbox v-model="form.autoRemove">{{ $t('container.autoRemove') }}</el-checkbox>
|
<el-checkbox v-model="dialogData.rowData!.autoRemove">
|
||||||
|
{{ $t('container.autoRemove') }}
|
||||||
|
</el-checkbox>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('container.cpuShare')" prop="cpuShares">
|
<el-form-item :label="$t('container.cpuShare')" prop="cpuShares">
|
||||||
<el-input style="width: 40%" v-model.number="form.cpuShares" />
|
<el-input style="width: 40%" v-model.number="dialogData.rowData!.cpuShares" />
|
||||||
<span class="input-help">{{ $t('container.cpuShareHelper') }}</span>
|
<span class="input-help">{{ $t('container.cpuShareHelper') }}</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('container.cpuQuota')" prop="nanoCPUs">
|
<el-form-item :label="$t('container.cpuQuota')" prop="nanoCPUs">
|
||||||
<el-input type="number" style="width: 40%" v-model.number="form.nanoCPUs">
|
<el-input type="number" style="width: 40%" v-model.number="dialogData.rowData!.nanoCPUs">
|
||||||
<template #append>
|
<template #append>
|
||||||
<el-select v-model="form.cpuUnit" disabled style="width: 85px">
|
<el-select v-model="dialogData.rowData!.cpuUnit" disabled style="width: 85px">
|
||||||
<el-option label="Core" value="Core" />
|
<el-option label="Core" value="Core" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -96,9 +105,13 @@
|
||||||
<span class="input-help">{{ $t('container.limitHelper') }}</span>
|
<span class="input-help">{{ $t('container.limitHelper') }}</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('container.memoryLimit')" prop="memoryItem">
|
<el-form-item :label="$t('container.memoryLimit')" prop="memoryItem">
|
||||||
<el-input style="width: 40%" v-model.number="form.memoryItem">
|
<el-input style="width: 40%" v-model.number="dialogData.rowData!.memoryItem">
|
||||||
<template #append>
|
<template #append>
|
||||||
<el-select v-model="form.memoryUnit" placeholder="Select" style="width: 85px">
|
<el-select
|
||||||
|
v-model="dialogData.rowData!.memoryUnit"
|
||||||
|
placeholder="Select"
|
||||||
|
style="width: 85px"
|
||||||
|
>
|
||||||
<el-option label="KB" value="KB" />
|
<el-option label="KB" value="KB" />
|
||||||
<el-option label="MB" value="MB" />
|
<el-option label="MB" value="MB" />
|
||||||
<el-option label="GB" value="GB" />
|
<el-option label="GB" value="GB" />
|
||||||
|
|
@ -110,7 +123,7 @@
|
||||||
<el-form-item :label="$t('container.mount')">
|
<el-form-item :label="$t('container.mount')">
|
||||||
<el-card style="width: 100%">
|
<el-card style="width: 100%">
|
||||||
<table style="width: 100%" class="tab-table">
|
<table style="width: 100%" class="tab-table">
|
||||||
<tr v-if="form.volumes.length !== 0">
|
<tr v-if="dialogData.rowData!.volumes.length !== 0">
|
||||||
<th scope="col" width="39%" align="left">
|
<th scope="col" width="39%" align="left">
|
||||||
<label>{{ $t('container.serverPath') }}</label>
|
<label>{{ $t('container.serverPath') }}</label>
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -122,7 +135,7 @@
|
||||||
</th>
|
</th>
|
||||||
<th align="left"></th>
|
<th align="left"></th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="(row, index) in form.volumes" :key="index">
|
<tr v-for="(row, index) in dialogData.rowData!.volumes" :key="index">
|
||||||
<td width="39%">
|
<td width="39%">
|
||||||
<el-select
|
<el-select
|
||||||
class="widthClass"
|
class="widthClass"
|
||||||
|
|
@ -169,23 +182,24 @@
|
||||||
<el-input
|
<el-input
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:placeholder="$t('container.tagHelper')"
|
:placeholder="$t('container.tagHelper')"
|
||||||
:autosize="{ minRows: 2, maxRows: 4 }"
|
:autosize="{ minRows: 2, maxRows: 10 }"
|
||||||
v-model="form.labelsStr"
|
v-model="dialogData.rowData!.labelsStr"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('container.env')" prop="envStr">
|
<el-form-item :label="$t('container.env')" prop="envStr">
|
||||||
<el-input
|
<el-input
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:placeholder="$t('container.tagHelper')"
|
:placeholder="$t('container.tagHelper')"
|
||||||
:autosize="{ minRows: 2, maxRows: 4 }"
|
:autosize="{ minRows: 2, maxRows: 10 }"
|
||||||
v-model="form.envStr"
|
v-model="dialogData.rowData!.envStr"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('container.restartPolicy')" prop="restartPolicy">
|
<el-form-item :label="$t('container.restartPolicy')" prop="restartPolicy">
|
||||||
<el-radio-group v-model="form.restartPolicy">
|
<el-radio-group v-model="dialogData.rowData!.restartPolicy">
|
||||||
<el-radio label="unless-stopped">{{ $t('container.unlessStopped') }}</el-radio>
|
|
||||||
<el-radio label="on-failure">{{ $t('container.onFailure') }}</el-radio>
|
|
||||||
<el-radio label="no">{{ $t('container.no') }}</el-radio>
|
<el-radio label="no">{{ $t('container.no') }}</el-radio>
|
||||||
|
<el-radio label="always">{{ $t('container.always') }}</el-radio>
|
||||||
|
<el-radio label="on-failure">{{ $t('container.onFailure') }}</el-radio>
|
||||||
|
<el-radio label="unless-stopped">{{ $t('container.unlessStopped') }}</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
@ -210,73 +224,56 @@ import { Rules, checkNumberRange } from '@/global/form-rules';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { ElForm } from 'element-plus';
|
import { ElForm } from 'element-plus';
|
||||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||||
import { listImage, listVolume, createContainer } from '@/api/modules/container';
|
import { listImage, listVolume, createContainer, updateContainer } from '@/api/modules/container';
|
||||||
import { Container } from '@/api/interface/container';
|
import { Container } from '@/api/interface/container';
|
||||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||||
import { checkIp, checkPort } from '@/utils/util';
|
import { checkIp, checkPort, computeSize } from '@/utils/util';
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
interface DialogProps {
|
||||||
|
title: string;
|
||||||
|
rowData?: Container.ContainerHelper;
|
||||||
|
getTableList?: () => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = ref<string>('');
|
||||||
const drawerVisiable = ref(false);
|
const drawerVisiable = ref(false);
|
||||||
const form = reactive({
|
|
||||||
name: '',
|
|
||||||
image: '',
|
|
||||||
cmdStr: '',
|
|
||||||
cmd: [] as Array<string>,
|
|
||||||
publishAllPorts: false,
|
|
||||||
exposedPorts: [] as Array<Container.Port>,
|
|
||||||
cpuShares: 1024,
|
|
||||||
nanoCPUs: 0,
|
|
||||||
memory: 0,
|
|
||||||
memoryItem: 0,
|
|
||||||
memoryUnit: 'MB',
|
|
||||||
cpuUnit: 'Core',
|
|
||||||
volumes: [] as Array<Container.Volume>,
|
|
||||||
autoRemove: false,
|
|
||||||
labels: [] as Array<string>,
|
|
||||||
labelsStr: '',
|
|
||||||
env: [] as Array<string>,
|
|
||||||
envStr: '',
|
|
||||||
restartPolicy: '',
|
|
||||||
});
|
|
||||||
const images = ref();
|
|
||||||
const volumes = ref();
|
|
||||||
|
|
||||||
const acceptParams = (): void => {
|
const dialogData = ref<DialogProps>({
|
||||||
handlReset();
|
title: '',
|
||||||
drawerVisiable.value = true;
|
});
|
||||||
|
const acceptParams = (params: DialogProps): void => {
|
||||||
|
dialogData.value = params;
|
||||||
|
title.value = i18n.global.t('commons.button.' + dialogData.value.title);
|
||||||
|
if (params.title === 'edit') {
|
||||||
|
dialogData.value.rowData.cpuUnit = 'Core';
|
||||||
|
let itemMem = computeSize(Number(dialogData.value.rowData.memory));
|
||||||
|
dialogData.value.rowData.memoryItem = itemMem.indexOf(' ') !== -1 ? Number(itemMem.split(' ')[0]) : 0;
|
||||||
|
dialogData.value.rowData.memoryUnit = itemMem.indexOf(' ') !== -1 ? itemMem.split(' ')[1] : 'MB';
|
||||||
|
let itemCmd = '';
|
||||||
|
for (const item of dialogData.value.rowData.cmd) {
|
||||||
|
itemCmd += `'${item}' `;
|
||||||
|
}
|
||||||
|
dialogData.value.rowData.cmdStr = itemCmd ? itemCmd.substring(0, itemCmd.length - 1) : '';
|
||||||
|
dialogData.value.rowData.labelsStr = dialogData.value.rowData.labels.join('\n');
|
||||||
|
dialogData.value.rowData.envStr = dialogData.value.rowData.env.join('\n');
|
||||||
|
for (const item of dialogData.value.rowData.exposedPorts) {
|
||||||
|
item.host = item.hostPort;
|
||||||
|
}
|
||||||
|
}
|
||||||
loadImageOptions();
|
loadImageOptions();
|
||||||
loadVolumeOptions();
|
loadVolumeOptions();
|
||||||
|
drawerVisiable.value = true;
|
||||||
};
|
};
|
||||||
|
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||||
|
|
||||||
const handlReset = () => {
|
const images = ref();
|
||||||
form.name = '';
|
const volumes = ref();
|
||||||
form.image = '';
|
|
||||||
form.cmdStr = '';
|
|
||||||
form.cmd = [];
|
|
||||||
form.publishAllPorts = false;
|
|
||||||
form.exposedPorts = [];
|
|
||||||
form.cpuShares = 1024;
|
|
||||||
form.nanoCPUs = 0;
|
|
||||||
form.memory = 0;
|
|
||||||
form.memoryItem = 0;
|
|
||||||
form.memoryUnit = 'MB';
|
|
||||||
form.cpuUnit = 'Core';
|
|
||||||
form.volumes = [];
|
|
||||||
form.autoRemove = false;
|
|
||||||
form.labels = [];
|
|
||||||
form.labelsStr = '';
|
|
||||||
form.env = [];
|
|
||||||
form.envStr = '';
|
|
||||||
form.restartPolicy = 'no';
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
drawerVisiable.value = false;
|
drawerVisiable.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
|
||||||
|
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
cpuShares: [Rules.number, checkNumberRange(2, 262144)],
|
cpuShares: [Rules.number, checkNumberRange(2, 262144)],
|
||||||
name: [Rules.requiredInput, Rules.name],
|
name: [Rules.requiredInput, Rules.name],
|
||||||
|
|
@ -296,10 +293,10 @@ const handlePortsAdd = () => {
|
||||||
hostPort: '',
|
hostPort: '',
|
||||||
protocol: 'tcp',
|
protocol: 'tcp',
|
||||||
};
|
};
|
||||||
form.exposedPorts.push(item);
|
dialogData.value.rowData!.exposedPorts.push(item);
|
||||||
};
|
};
|
||||||
const handlePortsDelete = (index: number) => {
|
const handlePortsDelete = (index: number) => {
|
||||||
form.exposedPorts.splice(index, 1);
|
dialogData.value.rowData!.exposedPorts.splice(index, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVolumesAdd = () => {
|
const handleVolumesAdd = () => {
|
||||||
|
|
@ -308,10 +305,10 @@ const handleVolumesAdd = () => {
|
||||||
containerDir: '',
|
containerDir: '',
|
||||||
mode: 'rw',
|
mode: 'rw',
|
||||||
};
|
};
|
||||||
form.volumes.push(item);
|
dialogData.value.rowData!.volumes.push(item);
|
||||||
};
|
};
|
||||||
const handleVolumesDelete = (index: number) => {
|
const handleVolumesDelete = (index: number) => {
|
||||||
form.volumes.splice(index, 1);
|
dialogData.value.rowData!.volumes.splice(index, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadImageOptions = async () => {
|
const loadImageOptions = async () => {
|
||||||
|
|
@ -323,8 +320,8 @@ const loadVolumeOptions = async () => {
|
||||||
volumes.value = res.data;
|
volumes.value = res.data;
|
||||||
};
|
};
|
||||||
const onSubmit = async (formEl: FormInstance | undefined) => {
|
const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||||
if (form.volumes.length !== 0) {
|
if (dialogData.value.rowData!.volumes.length !== 0) {
|
||||||
for (const item of form.volumes) {
|
for (const item of dialogData.value.rowData!.volumes) {
|
||||||
if (!item.containerDir || !item.sourceDir) {
|
if (!item.containerDir || !item.sourceDir) {
|
||||||
MsgError(i18n.global.t('container.volumeHelper'));
|
MsgError(i18n.global.t('container.volumeHelper'));
|
||||||
return;
|
return;
|
||||||
|
|
@ -334,48 +331,78 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||||
if (!formEl) return;
|
if (!formEl) return;
|
||||||
formEl.validate(async (valid) => {
|
formEl.validate(async (valid) => {
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
if (form.envStr.length !== 0) {
|
if (dialogData.value.rowData!.envStr.length !== 0) {
|
||||||
form.env = form.envStr.split('\n');
|
dialogData.value.rowData!.env = dialogData.value.rowData!.envStr.split('\n');
|
||||||
}
|
}
|
||||||
if (form.labelsStr.length !== 0) {
|
if (dialogData.value.rowData!.labelsStr.length !== 0) {
|
||||||
form.labels = form.labelsStr.split('\n');
|
dialogData.value.rowData!.labels = dialogData.value.rowData!.labelsStr.split('\n');
|
||||||
}
|
}
|
||||||
if (form.cmdStr.length !== 0) {
|
if (dialogData.value.rowData!.cmdStr.length !== 0) {
|
||||||
form.cmd = form.cmdStr.split(' ');
|
let itemCmd = dialogData.value.rowData!.cmdStr.split(' ');
|
||||||
|
for (const cmd of itemCmd) {
|
||||||
|
if (cmd.startsWith(`'`) && cmd.endsWith(`'`) && cmd.length >= 3) {
|
||||||
|
dialogData.value.rowData!.cmd.push(cmd.substring(1, cmd.length - 2));
|
||||||
|
} else {
|
||||||
|
MsgError(i18n.global.t('container.commandHelper'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!checkPortValid()) {
|
if (!checkPortValid()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (form.memoryUnit) {
|
switch (dialogData.value.rowData!.memoryUnit) {
|
||||||
case 'KB':
|
case 'KB':
|
||||||
form.memory = form.memoryItem * 1024;
|
dialogData.value.rowData!.memory = dialogData.value.rowData!.memoryItem * 1024;
|
||||||
break;
|
break;
|
||||||
case 'MB':
|
case 'MB':
|
||||||
form.memory = form.memoryItem * 1024 * 1024;
|
dialogData.value.rowData!.memory = dialogData.value.rowData!.memoryItem * 1024 * 1024;
|
||||||
break;
|
break;
|
||||||
case 'GB':
|
case 'GB':
|
||||||
form.memory = form.memoryItem * 1024 * 1024 * 1024;
|
dialogData.value.rowData!.memory = dialogData.value.rowData!.memoryItem * 1024 * 1024 * 1024;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await createContainer(form)
|
if (dialogData.value.title === 'create') {
|
||||||
.then(() => {
|
await createContainer(dialogData.value.rowData!)
|
||||||
loading.value = false;
|
.then(() => {
|
||||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
loading.value = false;
|
||||||
emit('search');
|
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
drawerVisiable.value = false;
|
emit('search');
|
||||||
})
|
drawerVisiable.value = false;
|
||||||
.catch(() => {
|
})
|
||||||
loading.value = false;
|
.catch(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
i18n.global.t('container.updateContaienrHelper'),
|
||||||
|
i18n.global.t('commons.button.edit'),
|
||||||
|
{
|
||||||
|
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||||
|
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||||
|
},
|
||||||
|
).then(async () => {
|
||||||
|
await updateContainer(dialogData.value.rowData!)
|
||||||
|
.then(() => {
|
||||||
|
loading.value = false;
|
||||||
|
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
emit('search');
|
||||||
|
drawerVisiable.value = false;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkPortValid = () => {
|
const checkPortValid = () => {
|
||||||
if (form.exposedPorts.length === 0) {
|
if (dialogData.value.rowData!.exposedPorts.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
for (const port of form.exposedPorts) {
|
for (const port of dialogData.value.rowData!.exposedPorts) {
|
||||||
if (port.host.indexOf(':') !== -1) {
|
if (port.host.indexOf(':') !== -1) {
|
||||||
port.hostIP = port.host.split(':')[0];
|
port.hostIP = port.host.split(':')[0];
|
||||||
if (checkIp(port.hostIP)) {
|
if (checkIp(port.hostIP)) {
|
||||||
Loading…
Add table
Reference in a new issue