Browse Source

feat: 支持容器镜像升级操作 (#1393)

pull/1411/head
ssongliu 1 year ago committed by GitHub
parent
commit
3ead633343
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      backend/app/api/v1/container.go
  2. 9
      backend/app/dto/container.go
  3. 20
      backend/app/dto/image.go
  4. 46
      backend/app/service/container.go
  5. 2
      backend/app/service/snapshot.go
  6. 1
      backend/router/ro_container.go
  7. 60
      cmd/server/docs/docs.go
  8. 60
      cmd/server/docs/swagger.json
  9. 40
      cmd/server/docs/swagger.yaml
  10. 3
      frontend/src/api/modules/container.ts
  11. 11
      frontend/src/lang/modules/en.ts
  12. 9
      frontend/src/lang/modules/zh.ts
  13. 119
      frontend/src/views/container/container/index.vue
  14. 23
      frontend/src/views/container/container/operate/index.vue
  15. 141
      frontend/src/views/container/container/upgrade/index.vue

26
backend/app/api/v1/container.go

@ -245,6 +245,32 @@ func (b *BaseApi) ContainerCreate(c *gin.Context) {
helper.SuccessWithData(c, nil) helper.SuccessWithData(c, nil)
} }
// @Tags Container
// @Summary Upgrade container
// @Description 更新容器镜像
// @Accept json
// @Param request body dto.ContainerUpgrade true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /containers/upgrade [post]
// @x-panel-log {"bodyKeys":["name","image"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"更新容器镜像 [name][image]","formatEN":"upgrade container image [name][image]"}
func (b *BaseApi) ContainerUpgrade(c *gin.Context) {
var req dto.ContainerUpgrade
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.ContainerUpgrade(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Container // @Tags Container
// @Summary Clean container // @Summary Clean container
// @Description 容器清理 // @Description 容器清理

9
backend/app/dto/container.go

@ -41,7 +41,7 @@ type ContainerOperate struct {
PublishAllPorts bool `json:"publishAllPorts"` PublishAllPorts bool `json:"publishAllPorts"`
ExposedPorts []PortHelper `json:"exposedPorts"` ExposedPorts []PortHelper `json:"exposedPorts"`
Cmd []string `json:"cmd"` Cmd []string `json:"cmd"`
CPUShares int64 `josn:"cpuShares"` CPUShares int64 `json:"cpuShares"`
NanoCPUs int64 `json:"nanoCPUs"` NanoCPUs int64 `json:"nanoCPUs"`
Memory int64 `json:"memory"` Memory int64 `json:"memory"`
AutoRemove bool `json:"autoRemove"` AutoRemove bool `json:"autoRemove"`
@ -51,6 +51,11 @@ type ContainerOperate struct {
RestartPolicy string `json:"restartPolicy"` RestartPolicy string `json:"restartPolicy"`
} }
type ContainerUpgrade struct {
Name string `json:"name" validate:"required"`
Image string `json:"image" validate:"required"`
}
type ContainterStats struct { type ContainterStats struct {
CPUPercent float64 `json:"cpuPercent"` CPUPercent float64 `json:"cpuPercent"`
Memory float64 `json:"memory"` Memory float64 `json:"memory"`
@ -83,7 +88,7 @@ type ContainerOperation struct {
type ContainerPrune struct { type ContainerPrune struct {
PruneType string `json:"pruneType" validate:"required,oneof=container image volume network"` PruneType string `json:"pruneType" validate:"required,oneof=container image volume network"`
WithTagAll bool `josn:"withTagAll"` WithTagAll bool `json:"withTagAll"`
} }
type ContainerPruneReport struct { type ContainerPruneReport struct {

20
backend/app/dto/image.go

@ -10,35 +10,35 @@ type ImageInfo struct {
} }
type ImageLoad struct { type ImageLoad struct {
Path string `josn:"path" validate:"required"` Path string `json:"path" validate:"required"`
} }
type ImageBuild struct { type ImageBuild struct {
From string `josn:"from" validate:"required"` From string `json:"from" validate:"required"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Dockerfile string `josn:"dockerfile" validate:"required"` Dockerfile string `json:"dockerfile" validate:"required"`
Tags []string `josn:"tags"` Tags []string `json:"tags"`
} }
type ImagePull struct { type ImagePull struct {
RepoID uint `josn:"repoID"` RepoID uint `json:"repoID"`
ImageName string `josn:"imageName" validate:"required"` ImageName string `json:"imageName" validate:"required"`
} }
type ImageTag struct { type ImageTag struct {
RepoID uint `josn:"repoID"` RepoID uint `json:"repoID"`
SourceID string `json:"sourceID" validate:"required"` SourceID string `json:"sourceID" validate:"required"`
TargetName string `josn:"targetName" validate:"required"` TargetName string `json:"targetName" validate:"required"`
} }
type ImagePush struct { type ImagePush struct {
RepoID uint `josn:"repoID" validate:"required"` RepoID uint `json:"repoID" validate:"required"`
TagName string `json:"tagName" validate:"required"` TagName string `json:"tagName" validate:"required"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
} }
type ImageSave struct { type ImageSave struct {
TagName string `json:"tagName" validate:"required"` TagName string `json:"tagName" validate:"required"`
Path string `josn:"path" validate:"required"` Path string `json:"path" validate:"required"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
} }

46
backend/app/service/container.go

@ -43,6 +43,7 @@ type IContainerService interface {
ComposeOperation(req dto.ComposeOperation) error ComposeOperation(req dto.ComposeOperation) error
ContainerCreate(req dto.ContainerOperate) error ContainerCreate(req dto.ContainerOperate) error
ContainerUpdate(req dto.ContainerOperate) error ContainerUpdate(req dto.ContainerOperate) error
ContainerUpgrade(req dto.ContainerUpgrade) error
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error) ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
LoadResouceLimit() (*dto.ResourceLimit, error) LoadResouceLimit() (*dto.ResourceLimit, error)
ContainerLogClean(req dto.OperationWithName) error ContainerLogClean(req dto.OperationWithName) error
@ -241,9 +242,9 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate) error {
return err return err
} }
var config *container.Config var config container.Config
var hostConf *container.HostConfig var hostConf container.HostConfig
if err := loadConfigInfo(req, config, hostConf); err != nil { if err := loadConfigInfo(req, &config, &hostConf); err != nil {
return err return err
} }
@ -255,7 +256,7 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate) error {
return err return err
} }
} }
container, err := client.ContainerCreate(ctx, config, hostConf, &network.NetworkingConfig{}, &v1.Platform{}, req.Name) container, err := client.ContainerCreate(ctx, &config, &hostConf, &network.NetworkingConfig{}, &v1.Platform{}, req.Name)
if err != nil { if err != nil {
_ = client.ContainerRemove(ctx, req.Name, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true}) _ = client.ContainerRemove(ctx, req.Name, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true})
return err return err
@ -284,6 +285,7 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
data.Image = oldContainer.Config.Image data.Image = oldContainer.Config.Image
data.Cmd = oldContainer.Config.Cmd data.Cmd = oldContainer.Config.Cmd
data.Env = oldContainer.Config.Env data.Env = oldContainer.Config.Env
data.CPUShares = oldContainer.HostConfig.CPUShares
for key, val := range oldContainer.Config.Labels { for key, val := range oldContainer.Config.Labels {
data.Labels = append(data.Labels, fmt.Sprintf("%s=%s", key, val)) data.Labels = append(data.Labels, fmt.Sprintf("%s=%s", key, val))
} }
@ -357,6 +359,41 @@ func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
return nil return nil
} }
func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) 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
config.Image = req.Image
hostConf := oldContainer.HostConfig
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()
@ -648,6 +685,7 @@ func loadConfigInfo(req dto.ContainerOperate, config *container.Config, hostConf
config.ExposedPorts = exposeds config.ExposedPorts = exposeds
hostConf.AutoRemove = req.AutoRemove hostConf.AutoRemove = req.AutoRemove
hostConf.CPUShares = req.CPUShares
hostConf.PublishAllPorts = req.PublishAllPorts hostConf.PublishAllPorts = req.PublishAllPorts
hostConf.RestartPolicy = container.RestartPolicy{Name: req.RestartPolicy} hostConf.RestartPolicy = container.RestartPolicy{Name: req.RestartPolicy}
if req.RestartPolicy == "on-failure" { if req.RestartPolicy == "on-failure" {

2
backend/app/service/snapshot.go

@ -535,7 +535,7 @@ func (u *SnapshotService) handleDaemonJson(fileOp files.FileOp, operation string
if operation == "snapshot" || operation == "recover" { if operation == "snapshot" || operation == "recover" {
_, err := os.Stat(daemonJsonPath) _, err := os.Stat(daemonJsonPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
global.LOG.Info("no daemon.josn in snapshot and system now, nothing happened") global.LOG.Info("no daemon.json in snapshot and system now, nothing happened")
} }
if err == nil { if err == nil {
if err := fileOp.CopyFile(daemonJsonPath, target); err != nil { if err := fileOp.CopyFile(daemonJsonPath, target); err != nil {

1
backend/router/ro_container.go

@ -20,6 +20,7 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
baRouter.POST("", baseApi.ContainerCreate) baRouter.POST("", baseApi.ContainerCreate)
baRouter.POST("/update", baseApi.ContainerUpdate) baRouter.POST("/update", baseApi.ContainerUpdate)
baRouter.POST("/upgrade", baseApi.ContainerUpgrade)
baRouter.POST("/info", baseApi.ContainerInfo) 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)

60
cmd/server/docs/docs.go

@ -2695,6 +2695,49 @@ var doc = `{
} }
} }
}, },
"/containers/upgrade": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "更新容器镜像",
"consumes": [
"application/json"
],
"tags": [
"Container"
],
"summary": "Upgrade container",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ContainerUpgrade"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"name",
"image"
],
"formatEN": "upgrade container image [name][image]",
"formatZH": "更新容器镜像 [name][image]",
"paramKeys": []
}
}
},
"/containers/volume": { "/containers/volume": {
"post": { "post": {
"security": [ "security": [
@ -10694,7 +10737,7 @@ var doc = `{
"type": "string" "type": "string"
} }
}, },
"cpushares": { "cpuShares": {
"type": "integer" "type": "integer"
}, },
"env": { "env": {
@ -10800,6 +10843,21 @@ var doc = `{
} }
} }
}, },
"dto.ContainerUpgrade": {
"type": "object",
"required": [
"image",
"name"
],
"properties": {
"image": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"dto.ContainterStats": { "dto.ContainterStats": {
"type": "object", "type": "object",
"properties": { "properties": {

60
cmd/server/docs/swagger.json

@ -2681,6 +2681,49 @@
} }
} }
}, },
"/containers/upgrade": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "更新容器镜像",
"consumes": [
"application/json"
],
"tags": [
"Container"
],
"summary": "Upgrade container",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.ContainerUpgrade"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"name",
"image"
],
"formatEN": "upgrade container image [name][image]",
"formatZH": "更新容器镜像 [name][image]",
"paramKeys": []
}
}
},
"/containers/volume": { "/containers/volume": {
"post": { "post": {
"security": [ "security": [
@ -10680,7 +10723,7 @@
"type": "string" "type": "string"
} }
}, },
"cpushares": { "cpuShares": {
"type": "integer" "type": "integer"
}, },
"env": { "env": {
@ -10786,6 +10829,21 @@
} }
} }
}, },
"dto.ContainerUpgrade": {
"type": "object",
"required": [
"image",
"name"
],
"properties": {
"image": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"dto.ContainterStats": { "dto.ContainterStats": {
"type": "object", "type": "object",
"properties": { "properties": {

40
cmd/server/docs/swagger.yaml

@ -261,7 +261,7 @@ definitions:
items: items:
type: string type: string
type: array type: array
cpushares: cpuShares:
type: integer type: integer
env: env:
items: items:
@ -334,6 +334,16 @@ definitions:
spaceReclaimed: spaceReclaimed:
type: integer type: integer
type: object type: object
dto.ContainerUpgrade:
properties:
image:
type: string
name:
type: string
required:
- image
- name
type: object
dto.ContainterStats: dto.ContainterStats:
properties: properties:
cache: cache:
@ -5070,6 +5080,34 @@ paths:
formatEN: update container [name][image] formatEN: update container [name][image]
formatZH: 更新容器 [name][image] formatZH: 更新容器 [name][image]
paramKeys: [] paramKeys: []
/containers/upgrade:
post:
consumes:
- application/json
description: 更新容器镜像
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.ContainerUpgrade'
responses:
"200":
description: ""
security:
- ApiKeyAuth: []
summary: Upgrade container
tags:
- Container
x-panel-log:
BeforeFuntions: []
bodyKeys:
- name
- image
formatEN: upgrade container image [name][image]
formatZH: 更新容器镜像 [name][image]
paramKeys: []
/containers/volume: /containers/volume:
post: post:
consumes: consumes:

3
frontend/src/api/modules/container.ts

@ -14,6 +14,9 @@ export const createContainer = (params: Container.ContainerHelper) => {
export const updateContainer = (params: Container.ContainerHelper) => { export const updateContainer = (params: Container.ContainerHelper) => {
return http.post(`/containers/update`, params, 3000000); return http.post(`/containers/update`, params, 3000000);
}; };
export const upgradeContainer = (name: string, image: string) => {
return http.post(`/containers/upgrade`, { name: name, image: image }, 3000000);
};
export const loadContainerInfo = (name: string) => { export const loadContainerInfo = (name: string) => {
return http.post<Container.ContainerHelper>(`/containers/info`, { name: name }); return http.post<Container.ContainerHelper>(`/containers/info`, { name: name });
}; };

11
frontend/src/lang/modules/en.ts

@ -491,11 +491,20 @@ 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',
upgrade: 'Upgrade',
upgradeHelper: 'This operation only supports upgrading container versions.',
upgradeWarning: 'The target version is lower than the original image version. Please try again!',
upgradeWarning2:
'The upgrade operation requires rebuilding the container, and any non-persistent data will be lost. Do you want to continue?',
oldImage: 'Current image',
targetImage: 'Target image',
appHelper:
'This container is sourced from the application store. Upgrading it may cause the service to be unavailable. Do you want to continue?',
port: 'Port', port: 'Port',
server: 'Host', server: 'Host',
serverExample: 'e.g. 80, 80-88, ip:80 or ip:80-88', serverExample: 'e.g. 80, 80-88, ip:80 or ip:80-88',

9
frontend/src/lang/modules/zh.ts

@ -496,11 +496,18 @@ const message = {
user: '用户', user: '用户',
command: '命令', command: '命令',
commandHelper: '请输入正确的命令多个命令空格分割',
custom: '自定义', custom: '自定义',
containerTerminal: '终端', containerTerminal: '终端',
emptyUser: '为空时将使用容器默认的用户登录', emptyUser: '为空时将使用容器默认的用户登录',
upgrade: '升级',
upgradeHelper: '该操作仅支持容器版本升级',
upgradeWarning: '当前目标版本低于原镜像版本请重新输入',
upgradeWarning2: '升级操作需要重建容器任何未持久化的数据将会丢失是否继续',
oldImage: '当前镜像',
targetImage: '目标镜像',
appHelper: '该容器来源于应用商店升级可能导致该服务不可用是否继续',
port: '端口', port: '端口',
server: '服务器', server: '服务器',
serverExample: ' 80, 80-88, ip:80 或者 ip:80-88', serverExample: ' 80, 80-88, ip:80 或者 ip:80-88',

119
frontend/src/views/container/container/index.vue

@ -16,25 +16,25 @@
{{ $t('container.containerPrune') }} {{ $t('container.containerPrune') }}
</el-button> </el-button>
<el-button-group style="margin-left: 10px"> <el-button-group style="margin-left: 10px">
<el-button :disabled="checkStatus('start')" @click="onOperate('start')"> <el-button :disabled="checkStatus('start', null)" @click="onOperate('start', null)">
{{ $t('container.start') }} {{ $t('container.start') }}
</el-button> </el-button>
<el-button :disabled="checkStatus('stop')" @click="onOperate('stop')"> <el-button :disabled="checkStatus('stop', null)" @click="onOperate('stop', null)">
{{ $t('container.stop') }} {{ $t('container.stop') }}
</el-button> </el-button>
<el-button :disabled="checkStatus('restart')" @click="onOperate('restart')"> <el-button :disabled="checkStatus('restart', null)" @click="onOperate('restart', null)">
{{ $t('container.restart') }} {{ $t('container.restart') }}
</el-button> </el-button>
<el-button :disabled="checkStatus('kill')" @click="onOperate('kill')"> <el-button :disabled="checkStatus('kill', null)" @click="onOperate('kill', null)">
{{ $t('container.kill') }} {{ $t('container.kill') }}
</el-button> </el-button>
<el-button :disabled="checkStatus('pause')" @click="onOperate('pause')"> <el-button :disabled="checkStatus('pause', null)" @click="onOperate('pause', null)">
{{ $t('container.pause') }} {{ $t('container.pause') }}
</el-button> </el-button>
<el-button :disabled="checkStatus('unpause')" @click="onOperate('unpause')"> <el-button :disabled="checkStatus('unpause', null)" @click="onOperate('unpause', null)">
{{ $t('container.unpause') }} {{ $t('container.unpause') }}
</el-button> </el-button>
<el-button :disabled="checkStatus('remove')" @click="onOperate('remove')"> <el-button :disabled="checkStatus('remove', null)" @click="onOperate('remove', null)">
{{ $t('container.remove') }} {{ $t('container.remove') }}
</el-button> </el-button>
</el-button-group> </el-button-group>
@ -110,7 +110,7 @@
/> />
<fu-table-operations <fu-table-operations
width="300px" width="300px"
:ellipsis="10" :ellipsis="3"
:buttons="buttons" :buttons="buttons"
:label="$t('commons.table.operate')" :label="$t('commons.table.operate')"
fix fix
@ -123,7 +123,8 @@
<ReNameDialog @search="search" ref="dialogReNameRef" /> <ReNameDialog @search="search" ref="dialogReNameRef" />
<ContainerLogDialog ref="dialogContainerLogRef" /> <ContainerLogDialog ref="dialogContainerLogRef" />
<CreateDialog @search="search" ref="dialogOperateRef" /> <OperateDialog @search="search" ref="dialogOperateRef" />
<UpgraeDialog @search="search" ref="dialogUpgradeRef" />
<MonitorDialog ref="dialogMonitorRef" /> <MonitorDialog ref="dialogMonitorRef" />
<TerminalDialog ref="dialogTerminalRef" /> <TerminalDialog ref="dialogTerminalRef" />
</div> </div>
@ -133,7 +134,8 @@
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/operate/index.vue'; import OperateDialog from '@/views/container/container/operate/index.vue';
import UpgraeDialog from '@/views/container/container/upgrade/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';
@ -164,6 +166,7 @@ const paginationConfig = reactive({
total: 0, total: 0,
}); });
const searchName = ref(); const searchName = ref();
const dialogUpgradeRef = ref();
const dockerStatus = ref('Running'); const dockerStatus = ref('Running');
const loadStatus = async () => { const loadStatus = async () => {
@ -231,11 +234,10 @@ const onOpenDialog = async (
cmd: [], cmd: [],
cmdStr: '', cmdStr: '',
exposedPorts: [], exposedPorts: [],
cpuShares: 1024,
nanoCPUs: 0, nanoCPUs: 0,
memory: 0, memory: 0,
memoryItem: 0, memoryItem: 0,
memoryUnit: 'MB',
cpuUnit: 'Core',
volumes: [], volumes: [],
labels: [], labels: [],
env: [], env: [],
@ -297,34 +299,35 @@ const onClean = () => {
}); });
}; };
const checkStatus = (operation: string) => { const checkStatus = (operation: string, row: Container.ContainerInfo | null) => {
if (selects.value.length < 1) { let opList = row ? [row] : selects.value;
if (opList.length < 1) {
return true; return true;
} }
switch (operation) { switch (operation) {
case 'start': case 'start':
for (const item of selects.value) { for (const item of opList) {
if (item.state === 'running') { if (item.state === 'running') {
return true; return true;
} }
} }
return false; return false;
case 'stop': case 'stop':
for (const item of selects.value) { for (const item of opList) {
if (item.state === 'stopped' || item.state === 'exited') { if (item.state === 'stopped' || item.state === 'exited') {
return true; return true;
} }
} }
return false; return false;
case 'pause': case 'pause':
for (const item of selects.value) { for (const item of opList) {
if (item.state === 'paused' || item.state === 'exited') { if (item.state === 'paused' || item.state === 'exited') {
return true; return true;
} }
} }
return false; return false;
case 'unpause': case 'unpause':
for (const item of selects.value) { for (const item of opList) {
if (item.state !== 'paused') { if (item.state !== 'paused') {
return true; return true;
} }
@ -332,9 +335,10 @@ const checkStatus = (operation: string) => {
return false; return false;
} }
}; };
const onOperate = async (operation: string) => { const onOperate = async (operation: string, row: Container.ContainerInfo | null) => {
let opList = row ? [row] : selects.value;
let msg = i18n.global.t('container.operatorHelper', [i18n.global.t('container.' + operation)]); let msg = i18n.global.t('container.operatorHelper', [i18n.global.t('container.' + operation)]);
for (const item of selects.value) { for (const item of opList) {
if (item.isFromApp) { if (item.isFromApp) {
msg = i18n.global.t('container.operatorAppHelper', [i18n.global.t('container.' + operation)]); msg = i18n.global.t('container.operatorAppHelper', [i18n.global.t('container.' + operation)]);
break; break;
@ -346,7 +350,7 @@ const onOperate = async (operation: string) => {
type: 'info', type: 'info',
}).then(() => { }).then(() => {
let ps = []; let ps = [];
for (const item of selects.value) { for (const item of opList) {
const param = { const param = {
name: item.name, name: item.name,
operation: operation, operation: operation,
@ -384,6 +388,12 @@ const buttons = [
onTerminal(row); onTerminal(row);
}, },
}, },
{
label: i18n.global.t('commons.button.log'),
click: (row: Container.ContainerInfo) => {
dialogContainerLogRef.value!.acceptParams({ containerID: row.containerID, container: row.name });
},
},
{ {
label: i18n.global.t('container.monitor'), label: i18n.global.t('container.monitor'),
disabled: (row: Container.ContainerInfo) => { disabled: (row: Container.ContainerInfo) => {
@ -393,6 +403,12 @@ const buttons = [
onMonitor(row); onMonitor(row);
}, },
}, },
{
label: i18n.global.t('container.upgrade'),
click: (row: Container.ContainerInfo) => {
dialogUpgradeRef.value!.acceptParams({ container: row.name, image: row.imageName, fromApp: row.isFromApp });
},
},
{ {
label: i18n.global.t('container.rename'), label: i18n.global.t('container.rename'),
click: (row: Container.ContainerInfo) => { click: (row: Container.ContainerInfo) => {
@ -403,9 +419,66 @@ const buttons = [
}, },
}, },
{ {
label: i18n.global.t('commons.button.log'), label: i18n.global.t('container.start'),
click: (row: Container.ContainerInfo) => { click: (row: Container.ContainerInfo) => {
dialogContainerLogRef.value!.acceptParams({ containerID: row.containerID, container: row.name }); onOperate('start', row);
},
disabled: (row: any) => {
return checkStatus('start', row);
},
},
{
label: i18n.global.t('container.stop'),
click: (row: Container.ContainerInfo) => {
onOperate('stop', row);
},
disabled: (row: any) => {
return checkStatus('stop', row);
},
},
{
label: i18n.global.t('container.restart'),
click: (row: Container.ContainerInfo) => {
onOperate('restart', row);
},
disabled: (row: any) => {
return checkStatus('restart', row);
},
},
{
label: i18n.global.t('container.kill'),
click: (row: Container.ContainerInfo) => {
onOperate('kill', row);
},
disabled: (row: any) => {
return checkStatus('kill', row);
},
},
{
label: i18n.global.t('container.pause'),
click: (row: Container.ContainerInfo) => {
onOperate('pause', row);
},
disabled: (row: any) => {
return checkStatus('pause', row);
},
},
{
label: i18n.global.t('container.unpause'),
click: (row: Container.ContainerInfo) => {
onOperate('unpause', row);
},
disabled: (row: any) => {
return checkStatus('unpause', row);
},
},
{
label: i18n.global.t('container.remove'),
click: (row: Container.ContainerInfo) => {
onOperate('remove', row);
},
disabled: (row: any) => {
return checkStatus('remove', row);
}, },
}, },
]; ];

23
frontend/src/views/container/container/operate/index.vue

@ -121,7 +121,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="dialogData.rowData!.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>
@ -252,9 +252,12 @@ const acceptParams = (params: DialogProps): void => {
dialogData.value.rowData.cmdStr = itemCmd ? itemCmd.substring(0, itemCmd.length - 1) : ''; dialogData.value.rowData.cmdStr = itemCmd ? itemCmd.substring(0, itemCmd.length - 1) : '';
dialogData.value.rowData.labelsStr = dialogData.value.rowData.labels.join('\n'); dialogData.value.rowData.labelsStr = dialogData.value.rowData.labels.join('\n');
dialogData.value.rowData.envStr = dialogData.value.rowData.env.join('\n'); dialogData.value.rowData.envStr = dialogData.value.rowData.env.join('\n');
dialogData.value.rowData.exposedPorts = dialogData.value.rowData.exposedPorts || [];
for (const item of dialogData.value.rowData.exposedPorts) { for (const item of dialogData.value.rowData.exposedPorts) {
item.host = item.hostPort; item.host = item.hostPort;
} }
dialogData.value.rowData.volumes = dialogData.value.rowData.volumes || [];
console.log(dialogData.value.rowData.cpuShares);
} }
loadLimit(); loadLimit();
loadImageOptions(); loadImageOptions();
@ -337,20 +340,18 @@ 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 (dialogData.value.rowData!.envStr.length !== 0) { if (dialogData.value.rowData?.envStr) {
dialogData.value.rowData!.env = dialogData.value.rowData!.envStr.split('\n'); dialogData.value.rowData.env = dialogData.value.rowData!.envStr.split('\n');
} }
if (dialogData.value.rowData!.labelsStr.length !== 0) { if (dialogData.value.rowData?.labelsStr) {
dialogData.value.rowData!.labels = dialogData.value.rowData!.labelsStr.split('\n'); dialogData.value.rowData!.labels = dialogData.value.rowData!.labelsStr.split('\n');
} }
if (dialogData.value.rowData!.cmdStr.length !== 0) { dialogData.value.rowData!.cmd = [];
let itemCmd = dialogData.value.rowData!.cmdStr.split(' '); if (dialogData.value.rowData?.cmdStr) {
let itemCmd = dialogData.value.rowData!.cmdStr.split(`'`);
for (const cmd of itemCmd) { for (const cmd of itemCmd) {
if (cmd.startsWith(`'`) && cmd.endsWith(`'`) && cmd.length >= 3) { if (cmd && cmd !== ' ') {
dialogData.value.rowData!.cmd.push(cmd.substring(1, cmd.length - 2)); dialogData.value.rowData!.cmd.push(cmd);
} else {
MsgError(i18n.global.t('container.commandHelper'));
return;
} }
} }
} }

141
frontend/src/views/container/container/upgrade/index.vue

@ -0,0 +1,141 @@
<template>
<el-drawer v-model="drawerVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="30%">
<template #header>
<DrawerHeader :header="$t('container.upgrade')" :resource="form.name" :back="handleClose" />
</template>
<el-alert v-if="form.fromApp" style="margin-bottom: 20px" :closable="false" type="error">
<template #default>
<span>
<span>{{ $t('container.appHelper') }}</span>
</span>
</template>
</el-alert>
<el-form @submit.prevent ref="formRef" v-loading="loading" :model="form" label-position="top">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('container.oldImage')" prop="oldImage">
<el-tag>{{ form.imageName }}:{{ form.oldTag }}</el-tag>
</el-form-item>
<el-form-item :label="$t('container.targetImage')" prop="newTag" :rules="Rules.requiredInput">
<el-input v-model="form.newTag">
<template #prefix>{{ form.imageName }}:</template>
</el-input>
<span class="input-help">{{ $t('container.upgradeHelper') }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button :disabled="loading" @click="drawerVisiable = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button :disabled="loading" type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { upgradeContainer } from '@/api/modules/container';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess, MsgWarning } from '@/utils/message';
import { ElForm } from 'element-plus';
import { reactive, ref } from 'vue';
import DrawerHeader from '@/components/drawer-header/index.vue';
const loading = ref(false);
const form = reactive({
name: '',
imageName: '',
oldTag: '',
newTag: '',
fromApp: false,
});
const formRef = ref<FormInstance>();
const drawerVisiable = ref<boolean>(false);
type FormInstance = InstanceType<typeof ElForm>;
interface DialogProps {
container: string;
image: string;
fromApp: boolean;
}
const acceptParams = (props: DialogProps): void => {
form.name = props.container;
form.imageName = props.image.indexOf(':') !== -1 ? props.image.split(':')[0] : props.image;
form.oldTag = props.image.indexOf(':') !== -1 ? props.image.split(':')[1] : 'latest';
form.newTag = '';
form.fromApp = props.fromApp;
drawerVisiable.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
if (!compareVersion(form.newTag, form.oldTag)) {
MsgWarning(i18n.global.t('container.upgradeWarning'));
return;
}
ElMessageBox.confirm(i18n.global.t('container.upgradeWarning2'), i18n.global.t('container.upgrade'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
loading.value = true;
await upgradeContainer(form.name, form.imageName + ':' + form.newTag)
.then(() => {
loading.value = false;
emit('search');
drawerVisiable.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
});
});
};
const handleClose = async () => {
drawerVisiable.value = false;
emit('search');
};
function compareVersion(vNew, vOld) {
if (vNew === 'latest') {
return true;
}
let v1 = vNew
.replace('-', '.')
.replace(/[^\d.]/g, '')
.split('.');
let v2 = vOld
.replace('-', '.')
.replace(/[^\d.]/g, '')
.split('.');
for (let i = 0; i < Math.max(v1.length, v2.length); i++) {
let num1 = parseInt(v1[i] || 0);
let num2 = parseInt(v2[i] || 0);
if (num1 > num2) {
return true;
} else if (num1 < num2) {
return false;
}
}
return false;
}
defineExpose({
acceptParams,
});
</script>
Loading…
Cancel
Save