Browse Source

feat: 容器创建编辑增加 cpu 、内存最大限制 (#1383)

pull/1393/head
ssongliu 1 year ago committed by GitHub
parent
commit
c82e20efd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      backend/app/api/v1/container.go
  2. 5
      backend/app/dto/container.go
  3. 20
      backend/app/service/container.go
  4. 1
      backend/router/ro_container.go
  5. 30
      cmd/server/docs/docs.go
  6. 30
      cmd/server/docs/swagger.json
  7. 18
      cmd/server/docs/swagger.yaml
  8. 6
      frontend/src/api/interface/container.ts
  9. 7
      frontend/src/api/modules/container.ts
  10. 2
      frontend/src/lang/modules/en.ts
  11. 2
      frontend/src/lang/modules/zh.ts
  12. 6
      frontend/src/views/container/compose/detail/index.vue
  13. 4
      frontend/src/views/container/container/index.vue
  14. 4
      frontend/src/views/container/container/monitor/index.vue
  15. 69
      frontend/src/views/container/container/operate/index.vue
  16. 4
      frontend/src/views/container/container/rename/index.vue

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

@ -205,6 +205,20 @@ func (b *BaseApi) ContainerInfo(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Summary Load container limis
// @Description 获取容器限制
// @Success 200 {object} dto.ResourceLimit
// @Security ApiKeyAuth
// @Router /containers/limit [get]
func (b *BaseApi) LoadResouceLimit(c *gin.Context) {
data, err := containerService.LoadResouceLimit()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Container
// @Summary Create container
// @Description 创建容器

5
backend/app/dto/container.go

@ -30,6 +30,11 @@ type ContainerInfo struct {
IsFromCompose bool `json:"isFromCompose"`
}
type ResourceLimit struct {
CPU int `json:"cpu"`
Memory int `json:"memory"`
}
type ContainerOperate struct {
Name string `json:"name"`
Image string `json:"image"`

20
backend/app/service/container.go

@ -27,6 +27,8 @@ import (
"github.com/docker/go-connections/nat"
"github.com/gorilla/websocket"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/mem"
)
type ContainerService struct{}
@ -42,6 +44,7 @@ type IContainerService interface {
ContainerCreate(req dto.ContainerOperate) error
ContainerUpdate(req dto.ContainerOperate) error
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
LoadResouceLimit() (*dto.ResourceLimit, error)
ContainerLogClean(req dto.OperationWithName) error
ContainerOperation(req dto.ContainerOperation) error
ContainerLogs(wsConn *websocket.Conn, container, since, tail string, follow bool) error
@ -215,6 +218,23 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) (dto.ContainerPruneRepo
return report, nil
}
func (u *ContainerService) LoadResouceLimit() (*dto.ResourceLimit, error) {
cpuCounts, err := cpu.Counts(false)
if err != nil {
return nil, fmt.Errorf("load cpu limit failed, err: %v", err)
}
memoryInfo, err := mem.VirtualMemory()
if err != nil {
return nil, fmt.Errorf("load memory limit failed, err: %v", err)
}
data := dto.ResourceLimit{
CPU: cpuCounts,
Memory: int(memoryInfo.Total),
}
return &data, nil
}
func (u *ContainerService) ContainerCreate(req dto.ContainerOperate) error {
client, err := docker.NewDockerClient()
if err != nil {

1
backend/router/ro_container.go

@ -23,6 +23,7 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
baRouter.POST("/info", baseApi.ContainerInfo)
baRouter.POST("/search", baseApi.SearchContainer)
baRouter.GET("/search/log", baseApi.ContainerLogs)
baRouter.GET("/limit", baseApi.LoadResouceLimit)
baRouter.POST("/clean/log", baseApi.CleanContainerLog)
baRouter.POST("/inspect", baseApi.Inspect)
baRouter.POST("/operate", baseApi.ContainerOperation)

30
cmd/server/docs/docs.go

@ -1853,6 +1853,25 @@ var doc = `{
}
}
},
"/containers/limit": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取容器限制",
"summary": "Load container limis",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.ResourceLimit"
}
}
}
}
},
"/containers/network": {
"post": {
"security": [
@ -12408,6 +12427,17 @@ var doc = `{
}
}
},
"dto.ResourceLimit": {
"type": "object",
"properties": {
"cpu": {
"type": "integer"
},
"memory": {
"type": "integer"
}
}
},
"dto.SSHConf": {
"type": "object",
"properties": {

30
cmd/server/docs/swagger.json

@ -1839,6 +1839,25 @@
}
}
},
"/containers/limit": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取容器限制",
"summary": "Load container limis",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.ResourceLimit"
}
}
}
}
},
"/containers/network": {
"post": {
"security": [
@ -12394,6 +12413,17 @@
}
}
},
"dto.ResourceLimit": {
"type": "object",
"properties": {
"cpu": {
"type": "integer"
},
"memory": {
"type": "integer"
}
}
},
"dto.SSHConf": {
"type": "object",
"properties": {

18
cmd/server/docs/swagger.yaml

@ -1422,6 +1422,13 @@ definitions:
used_memory_rss:
type: string
type: object
dto.ResourceLimit:
properties:
cpu:
type: integer
memory:
type: integer
type: object
dto.SSHConf:
properties:
file:
@ -4529,6 +4536,17 @@ paths:
summary: Container inspect
tags:
- Container
/containers/limit:
get:
description: 获取容器限制
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.ResourceLimit'
security:
- ApiKeyAuth: []
summary: Load container limis
/containers/network:
post:
consumes:

6
frontend/src/api/interface/container.ts

@ -10,18 +10,20 @@ export namespace Container {
name: string;
filters: string;
}
export interface ResourceLimit {
cpu: number;
memory: number;
}
export interface ContainerHelper {
name: string;
image: string;
cmdStr: string;
memoryUnit: string;
memoryItem: number;
cmd: Array<string>;
publishAllPorts: boolean;
exposedPorts: Array<Port>;
nanoCPUs: number;
cpuShares: number;
cpuUnit: string;
memory: number;
volumes: Array<Volume>;
autoRemove: boolean;

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

@ -5,6 +5,9 @@ import { Container } from '../interface/container';
export const searchContainer = (params: Container.ContainerSearch) => {
return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params, 400000);
};
export const loadResourceLimit = () => {
return http.get<Container.ResourceLimit>(`/containers/limit`);
};
export const createContainer = (params: Container.ContainerHelper) => {
return http.post(`/containers`, params, 3000000);
};
@ -17,10 +20,10 @@ export const loadContainerInfo = (name: string) => {
export const cleanContainerLog = (containerName: string) => {
return http.post(`/containers/clean/log`, { name: containerName });
};
export const ContainerStats = (id: string) => {
export const containerStats = (id: string) => {
return http.get<Container.ContainerStats>(`/containers/stats/${id}`);
};
export const ContainerOperator = (params: Container.ContainerOperate) => {
export const containerOperator = (params: Container.ContainerOperate) => {
return http.post(`/containers/operate`, params);
};
export const containerPrune = (params: Container.ContainerPrune) => {

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

@ -507,7 +507,7 @@ const message = {
autoRemove: 'Auto remove',
cpuQuota: 'NacosCPU',
memoryLimit: 'Memory',
limitHelper: 'If the limit is 0, the limit is turned off',
limitHelper: 'If you limit it to 0, then the limitation is turned off, and the maximum available is {0}.',
mount: 'Mount',
serverPath: 'Server path',
containerDir: 'Container path',

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

@ -512,7 +512,7 @@ const message = {
autoRemove: '容器退出后自动删除容器',
cpuQuota: 'CPU 限制',
memoryLimit: '内存限制',
limitHelper: '限制为 0 则关闭限制',
limitHelper: '限制为 0 则关闭限制最大可用为 {0}',
mount: '挂载卷',
serverPath: '服务器目录',
containerDir: '容器目录',

6
frontend/src/views/container/compose/detail/index.vue

@ -104,7 +104,6 @@
<CodemirrorDialog ref="mydetail" />
<ContainerLogDialog ref="dialogContainerLogRef" />
<CreateDialog @search="search" ref="dialogCreateRef" />
<MonitorDialog ref="dialogMonitorRef" />
<TerminalDialog ref="dialogTerminalRef" />
</template>
@ -115,14 +114,13 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import Tooltip from '@/components/tooltip/index.vue';
import CreateDialog from '@/views/container/container/create/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';
import CodemirrorDialog from '@/components/codemirror-dialog/index.vue';
import Status from '@/components/status/index.vue';
import { dateFormat } from '@/utils/util';
import { composeOperator, ContainerOperator, inspect, searchContainer } from '@/api/modules/container';
import { composeOperator, containerOperator, inspect, searchContainer } from '@/api/modules/container';
import { ElMessageBox } from 'element-plus';
import i18n from '@/lang';
import { Container } from '@/api/interface/container';
@ -240,7 +238,7 @@ const onOperate = async (operation: string) => {
operation: operation,
newName: '',
};
ps.push(ContainerOperator(param));
ps.push(containerOperator(param));
}
Promise.all(ps)
.then(() => {

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

@ -141,7 +141,7 @@ import CodemirrorDialog from '@/components/codemirror-dialog/index.vue';
import Status from '@/components/status/index.vue';
import { reactive, onMounted, ref } from 'vue';
import {
ContainerOperator,
containerOperator,
containerPrune,
inspect,
loadContainerInfo,
@ -352,7 +352,7 @@ const onOperate = async (operation: string) => {
operation: operation,
newName: '',
};
ps.push(ContainerOperator(param));
ps.push(containerOperator(param));
}
loading.value = true;
Promise.all(ps)

4
frontend/src/views/container/container/monitor/index.vue

@ -62,7 +62,7 @@
<script lang="ts" setup>
import { onBeforeUnmount, ref } from 'vue';
import { ContainerStats } from '@/api/modules/container';
import { containerStats } from '@/api/modules/container';
import { dateFormatForSecond } from '@/utils/util';
import VCharts from '@/components/v-charts/index.vue';
import i18n from '@/lang';
@ -125,7 +125,7 @@ const changeTimer = () => {
};
const loadData = async () => {
const res = await ContainerStats(dialogData.value.containerID);
const res = await containerStats(dialogData.value.containerID);
cpuDatas.value.push(res.data.cpuPercent.toFixed(2));
if (cpuDatas.value.length > 20) {
cpuDatas.value.splice(0, 1);

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

@ -94,31 +94,29 @@
<el-input style="width: 40%" v-model.number="dialogData.rowData!.cpuShares" />
<span class="input-help">{{ $t('container.cpuShareHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('container.cpuQuota')" prop="nanoCPUs">
<el-input type="number" style="width: 40%" v-model.number="dialogData.rowData!.nanoCPUs">
<el-form-item
:label="$t('container.cpuQuota')"
prop="nanoCPUs"
:rules="checkNumberRange(0, limits.cpu)"
>
<el-input style="width: 40%" v-model.number="dialogData.rowData!.nanoCPUs">
<template #append>
<el-select v-model="dialogData.rowData!.cpuUnit" disabled style="width: 85px">
<el-option label="Core" value="Core" />
</el-select>
<div style="width: 35px">{{ $t('home.coreUnit') }}</div>
</template>
</el-input>
<span class="input-help">{{ $t('container.limitHelper') }}</span>
<span class="input-help">
{{ $t('container.limitHelper', [limits.cpu]) }}{{ $t('home.coreUnit') }}
</span>
</el-form-item>
<el-form-item :label="$t('container.memoryLimit')" prop="memoryItem">
<el-form-item
:label="$t('container.memoryLimit')"
prop="memoryItem"
:rules="checkNumberRange(0, limits.memory)"
>
<el-input style="width: 40%" v-model.number="dialogData.rowData!.memoryItem">
<template #append>
<el-select
v-model="dialogData.rowData!.memoryUnit"
placeholder="Select"
style="width: 85px"
>
<el-option label="KB" value="KB" />
<el-option label="MB" value="MB" />
<el-option label="GB" value="GB" />
</el-select>
</template>
<template #append><div style="width: 35px">MB</div></template>
</el-input>
<span class="input-help">{{ $t('container.limitHelper') }}</span>
<span class="input-help">{{ $t('container.limitHelper', [limits.memory]) }}MB</span>
</el-form-item>
<el-form-item :label="$t('container.mount')">
<el-card style="width: 100%">
@ -224,10 +222,10 @@ import { Rules, checkNumberRange } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { listImage, listVolume, createContainer, updateContainer } from '@/api/modules/container';
import { listImage, listVolume, createContainer, updateContainer, loadResourceLimit } from '@/api/modules/container';
import { Container } from '@/api/interface/container';
import { MsgError, MsgSuccess } from '@/utils/message';
import { checkIp, checkPort, computeSize } from '@/utils/util';
import { checkIp, checkPort } from '@/utils/util';
const loading = ref(false);
interface DialogProps {
@ -246,10 +244,7 @@ const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
title.value = i18n.global.t('commons.button.' + dialogData.value.title);
if (params.title === 'edit') {
dialogData.value.rowData.cpuUnit = 'Core';
let itemMem = computeSize(Number(dialogData.value.rowData.memory));
dialogData.value.rowData.memoryItem = itemMem.indexOf(' ') !== -1 ? Number(itemMem.split(' ')[0]) : 0;
dialogData.value.rowData.memoryUnit = itemMem.indexOf(' ') !== -1 ? itemMem.split(' ')[1] : 'MB';
dialogData.value.rowData.memoryItem = Number((dialogData.value.rowData.memory / 1024 / 1024).toFixed(2));
let itemCmd = '';
for (const item of dialogData.value.rowData.cmd) {
itemCmd += `'${item}' `;
@ -261,6 +256,7 @@ const acceptParams = (params: DialogProps): void => {
item.host = item.hostPort;
}
}
loadLimit();
loadImageOptions();
loadVolumeOptions();
drawerVisiable.value = true;
@ -269,6 +265,10 @@ const emit = defineEmits<{ (e: 'search'): void }>();
const images = ref();
const volumes = ref();
const limits = ref<Container.ResourceLimit>({
cpu: null as number,
memory: null as number,
});
const handleClose = () => {
drawerVisiable.value = false;
@ -311,6 +311,12 @@ const handleVolumesDelete = (index: number) => {
dialogData.value.rowData!.volumes.splice(index, 1);
};
const loadLimit = async () => {
const res = await loadResourceLimit();
limits.value = res.data;
limits.value.memory = Number((limits.value.memory / 1024 / 1024).toFixed(2));
};
const loadImageOptions = async () => {
const res = await listImage();
images.value = res.data;
@ -351,17 +357,8 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
if (!checkPortValid()) {
return;
}
switch (dialogData.value.rowData!.memoryUnit) {
case 'KB':
dialogData.value.rowData!.memory = dialogData.value.rowData!.memoryItem * 1024;
break;
case 'MB':
dialogData.value.rowData!.memory = dialogData.value.rowData!.memoryItem * 1024 * 1024;
break;
case 'GB':
dialogData.value.rowData!.memory = dialogData.value.rowData!.memoryItem * 1024 * 1024 * 1024;
break;
}
dialogData.value.rowData!.memory = dialogData.value.rowData!.memoryItem * 1024 * 1024;
loading.value = true;
if (dialogData.value.title === 'create') {
await createContainer(dialogData.value.rowData!)

4
frontend/src/views/container/container/rename/index.vue

@ -26,7 +26,7 @@
</template>
<script lang="ts" setup>
import { ContainerOperator } from '@/api/modules/container';
import { containerOperator } from '@/api/modules/container';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
@ -54,7 +54,7 @@ const onSubmitName = async (formEl: FormInstance | undefined) => {
formEl.validate(async (valid) => {
if (!valid) return;
loading.value = true;
await ContainerOperator(renameForm)
await containerOperator(renameForm)
.then(() => {
loading.value = false;
emit('search');

Loading…
Cancel
Save