mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 08:19:15 +08:00
feat: 支持容器镜像升级操作 (#1393)
This commit is contained in:
parent
e71f765f2a
commit
3ead633343
@ -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 容器清理
|
||||
|
@ -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 {
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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" {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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:
|
||||
|
@ -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 });
|
||||
};
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -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
Normal file
141
frontend/src/views/container/container/upgrade/index.vue
Normal file
@ -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…
x
Reference in New Issue
Block a user