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)
}
// @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
// @Summary Clean container
// @Description 容器清理

9
backend/app/dto/container.go

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

20
backend/app/dto/image.go

@ -10,35 +10,35 @@ type ImageInfo struct {
}
type ImageLoad struct {
Path string `josn:"path" validate:"required"`
Path string `json:"path" validate:"required"`
}
type ImageBuild struct {
From string `josn:"from" validate:"required"`
From string `json:"from" validate:"required"`
Name string `json:"name" validate:"required"`
Dockerfile string `josn:"dockerfile" validate:"required"`
Tags []string `josn:"tags"`
Dockerfile string `json:"dockerfile" validate:"required"`
Tags []string `json:"tags"`
}
type ImagePull struct {
RepoID uint `josn:"repoID"`
ImageName string `josn:"imageName" validate:"required"`
RepoID uint `json:"repoID"`
ImageName string `json:"imageName" validate:"required"`
}
type ImageTag struct {
RepoID uint `josn:"repoID"`
RepoID uint `json:"repoID"`
SourceID string `json:"sourceID" validate:"required"`
TargetName string `josn:"targetName" validate:"required"`
TargetName string `json:"targetName" validate:"required"`
}
type ImagePush struct {
RepoID uint `josn:"repoID" validate:"required"`
RepoID uint `json:"repoID" validate:"required"`
TagName string `json:"tagName" validate:"required"`
Name string `json:"name" validate:"required"`
}
type ImageSave struct {
TagName string `json:"tagName" validate:"required"`
Path string `josn:"path" validate:"required"`
Path string `json:"path" 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
ContainerCreate(req dto.ContainerOperate) error
ContainerUpdate(req dto.ContainerOperate) error
ContainerUpgrade(req dto.ContainerUpgrade) error
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
LoadResouceLimit() (*dto.ResourceLimit, error)
ContainerLogClean(req dto.OperationWithName) error
@ -241,9 +242,9 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate) error {
return err
}
var config *container.Config
var hostConf *container.HostConfig
if err := loadConfigInfo(req, config, hostConf); err != nil {
var config container.Config
var hostConf container.HostConfig
if err := loadConfigInfo(req, &config, &hostConf); err != nil {
return err
}
@ -255,7 +256,7 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate) error {
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 {
_ = client.ContainerRemove(ctx, req.Name, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true})
return err
@ -284,6 +285,7 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
data.Image = oldContainer.Config.Image
data.Cmd = oldContainer.Config.Cmd
data.Env = oldContainer.Config.Env
data.CPUShares = oldContainer.HostConfig.CPUShares
for key, val := range oldContainer.Config.Labels {
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
}
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 {
var err error
ctx := context.Background()
@ -648,6 +685,7 @@ func loadConfigInfo(req dto.ContainerOperate, config *container.Config, hostConf
config.ExposedPorts = exposeds
hostConf.AutoRemove = req.AutoRemove
hostConf.CPUShares = req.CPUShares
hostConf.PublishAllPorts = req.PublishAllPorts
hostConf.RestartPolicy = container.RestartPolicy{Name: req.RestartPolicy}
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" {
_, err := os.Stat(daemonJsonPath)
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 := 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("/update", baseApi.ContainerUpdate)
baRouter.POST("/upgrade", baseApi.ContainerUpgrade)
baRouter.POST("/info", baseApi.ContainerInfo)
baRouter.POST("/search", baseApi.SearchContainer)
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": {
"post": {
"security": [
@ -10694,7 +10737,7 @@ var doc = `{
"type": "string"
}
},
"cpushares": {
"cpuShares": {
"type": "integer"
},
"env": {
@ -10800,6 +10843,21 @@ var doc = `{
}
}
},
"dto.ContainerUpgrade": {
"type": "object",
"required": [
"image",
"name"
],
"properties": {
"image": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"dto.ContainterStats": {
"type": "object",
"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": {
"post": {
"security": [
@ -10680,7 +10723,7 @@
"type": "string"
}
},
"cpushares": {
"cpuShares": {
"type": "integer"
},
"env": {
@ -10786,6 +10829,21 @@
}
}
},
"dto.ContainerUpgrade": {
"type": "object",
"required": [
"image",
"name"
],
"properties": {
"image": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"dto.ContainterStats": {
"type": "object",
"properties": {

40
cmd/server/docs/swagger.yaml

@ -261,7 +261,7 @@ definitions:
items:
type: string
type: array
cpushares:
cpuShares:
type: integer
env:
items:
@ -334,6 +334,16 @@ definitions:
spaceReclaimed:
type: integer
type: object
dto.ContainerUpgrade:
properties:
image:
type: string
name:
type: string
required:
- image
- name
type: object
dto.ContainterStats:
properties:
cache:
@ -5070,6 +5080,34 @@ paths:
formatEN: update container [name][image]
formatZH: 更新容器 [name][image]
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:
post:
consumes:

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

@ -14,6 +14,9 @@ export const createContainer = (params: Container.ContainerHelper) => {
export const updateContainer = (params: Container.ContainerHelper) => {
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) => {
return http.post<Container.ContainerHelper>(`/containers/info`, { name: name });
};

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

@ -491,11 +491,20 @@ const message = {
user: 'User',
command: 'Command',
commandHelper: 'Please enter the correct command, separated by spaces if there are multiple commands.',
custom: 'Custom',
emptyUser: 'When empty, you will log in as default',
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',
server: 'Host',
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: '用户',
command: '命令',
commandHelper: '请输入正确的命令多个命令空格分割',
custom: '自定义',
containerTerminal: '终端',
emptyUser: '为空时将使用容器默认的用户登录',
upgrade: '升级',
upgradeHelper: '该操作仅支持容器版本升级',
upgradeWarning: '当前目标版本低于原镜像版本请重新输入',
upgradeWarning2: '升级操作需要重建容器任何未持久化的数据将会丢失是否继续',
oldImage: '当前镜像',
targetImage: '目标镜像',
appHelper: '该容器来源于应用商店升级可能导致该服务不可用是否继续',
port: '端口',
server: '服务器',
serverExample: ' 80, 80-88, ip:80 或者 ip:80-88',

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

@ -16,25 +16,25 @@
{{ $t('container.containerPrune') }}
</el-button>
<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') }}
</el-button>
<el-button :disabled="checkStatus('stop')" @click="onOperate('stop')">
<el-button :disabled="checkStatus('stop', null)" @click="onOperate('stop', null)">
{{ $t('container.stop') }}
</el-button>
<el-button :disabled="checkStatus('restart')" @click="onOperate('restart')">
<el-button :disabled="checkStatus('restart', null)" @click="onOperate('restart', null)">
{{ $t('container.restart') }}
</el-button>
<el-button :disabled="checkStatus('kill')" @click="onOperate('kill')">
<el-button :disabled="checkStatus('kill', null)" @click="onOperate('kill', null)">
{{ $t('container.kill') }}
</el-button>
<el-button :disabled="checkStatus('pause')" @click="onOperate('pause')">
<el-button :disabled="checkStatus('pause', null)" @click="onOperate('pause', null)">
{{ $t('container.pause') }}
</el-button>
<el-button :disabled="checkStatus('unpause')" @click="onOperate('unpause')">
<el-button :disabled="checkStatus('unpause', null)" @click="onOperate('unpause', null)">
{{ $t('container.unpause') }}
</el-button>
<el-button :disabled="checkStatus('remove')" @click="onOperate('remove')">
<el-button :disabled="checkStatus('remove', null)" @click="onOperate('remove', null)">
{{ $t('container.remove') }}
</el-button>
</el-button-group>
@ -110,7 +110,7 @@
/>
<fu-table-operations
width="300px"
:ellipsis="10"
:ellipsis="3"
:buttons="buttons"
:label="$t('commons.table.operate')"
fix
@ -123,7 +123,8 @@
<ReNameDialog @search="search" ref="dialogReNameRef" />
<ContainerLogDialog ref="dialogContainerLogRef" />
<CreateDialog @search="search" ref="dialogOperateRef" />
<OperateDialog @search="search" ref="dialogOperateRef" />
<UpgraeDialog @search="search" ref="dialogUpgradeRef" />
<MonitorDialog ref="dialogMonitorRef" />
<TerminalDialog ref="dialogTerminalRef" />
</div>
@ -133,7 +134,8 @@
import Tooltip from '@/components/tooltip/index.vue';
import TableSetting from '@/components/table-setting/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 ContainerLogDialog from '@/views/container/container/log/index.vue';
import TerminalDialog from '@/views/container/container/terminal/index.vue';
@ -164,6 +166,7 @@ const paginationConfig = reactive({
total: 0,
});
const searchName = ref();
const dialogUpgradeRef = ref();
const dockerStatus = ref('Running');
const loadStatus = async () => {
@ -231,11 +234,10 @@ const onOpenDialog = async (
cmd: [],
cmdStr: '',
exposedPorts: [],
cpuShares: 1024,
nanoCPUs: 0,
memory: 0,
memoryItem: 0,
memoryUnit: 'MB',
cpuUnit: 'Core',
volumes: [],
labels: [],
env: [],
@ -297,34 +299,35 @@ const onClean = () => {
});
};
const checkStatus = (operation: string) => {
if (selects.value.length < 1) {
const checkStatus = (operation: string, row: Container.ContainerInfo | null) => {
let opList = row ? [row] : selects.value;
if (opList.length < 1) {
return true;
}
switch (operation) {
case 'start':
for (const item of selects.value) {
for (const item of opList) {
if (item.state === 'running') {
return true;
}
}
return false;
case 'stop':
for (const item of selects.value) {
for (const item of opList) {
if (item.state === 'stopped' || item.state === 'exited') {
return true;
}
}
return false;
case 'pause':
for (const item of selects.value) {
for (const item of opList) {
if (item.state === 'paused' || item.state === 'exited') {
return true;
}
}
return false;
case 'unpause':
for (const item of selects.value) {
for (const item of opList) {
if (item.state !== 'paused') {
return true;
}
@ -332,9 +335,10 @@ const checkStatus = (operation: string) => {
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)]);
for (const item of selects.value) {
for (const item of opList) {
if (item.isFromApp) {
msg = i18n.global.t('container.operatorAppHelper', [i18n.global.t('container.' + operation)]);
break;
@ -346,7 +350,7 @@ const onOperate = async (operation: string) => {
type: 'info',
}).then(() => {
let ps = [];
for (const item of selects.value) {
for (const item of opList) {
const param = {
name: item.name,
operation: operation,
@ -384,6 +388,12 @@ const buttons = [
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'),
disabled: (row: Container.ContainerInfo) => {
@ -393,6 +403,12 @@ const buttons = [
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'),
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) => {
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-card style="width: 100%">
<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">
<label>{{ $t('container.serverPath') }}</label>
</th>
@ -252,9 +252,12 @@ const acceptParams = (params: DialogProps): void => {
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');
dialogData.value.rowData.exposedPorts = dialogData.value.rowData.exposedPorts || [];
for (const item of dialogData.value.rowData.exposedPorts) {
item.host = item.hostPort;
}
dialogData.value.rowData.volumes = dialogData.value.rowData.volumes || [];
console.log(dialogData.value.rowData.cpuShares);
}
loadLimit();
loadImageOptions();
@ -337,20 +340,18 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
if (dialogData.value.rowData!.envStr.length !== 0) {
dialogData.value.rowData!.env = dialogData.value.rowData!.envStr.split('\n');
if (dialogData.value.rowData?.envStr) {
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');
}
if (dialogData.value.rowData!.cmdStr.length !== 0) {
let itemCmd = dialogData.value.rowData!.cmdStr.split(' ');
dialogData.value.rowData!.cmd = [];
if (dialogData.value.rowData?.cmdStr) {
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 (cmd && cmd !== ' ') {
dialogData.value.rowData!.cmd.push(cmd);
}
}
}

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