mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 08:19:15 +08:00
feat: 容器创建编辑增加 cpu 、内存最大限制 (#1383)
This commit is contained in:
parent
aa37e3885c
commit
c82e20efd7
@ -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 创建容器
|
||||
|
@ -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"`
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
@ -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) => {
|
||||
|
@ -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',
|
||||
|
@ -512,7 +512,7 @@ const message = {
|
||||
autoRemove: '容器退出后自动删除容器',
|
||||
cpuQuota: 'CPU 限制',
|
||||
memoryLimit: '内存限制',
|
||||
limitHelper: '限制为 0 则关闭限制',
|
||||
limitHelper: '限制为 0 则关闭限制,最大可用为 {0}',
|
||||
mount: '挂载卷',
|
||||
serverPath: '服务器目录',
|
||||
containerDir: '容器目录',
|
||||
|
@ -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(() => {
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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">
|
||||
<template #append>
|
||||
<el-select v-model="dialogData.rowData!.cpuUnit" disabled style="width: 85px">
|
||||
<el-option label="Core" value="Core" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="input-help">{{ $t('container.limitHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('container.memoryLimit')" prop="memoryItem">
|
||||
<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-form-item
|
||||
:label="$t('container.cpuQuota')"
|
||||
prop="nanoCPUs"
|
||||
:rules="checkNumberRange(0, limits.cpu)"
|
||||
>
|
||||
<el-option label="KB" value="KB" />
|
||||
<el-option label="MB" value="MB" />
|
||||
<el-option label="GB" value="GB" />
|
||||
</el-select>
|
||||
<el-input style="width: 40%" v-model.number="dialogData.rowData!.nanoCPUs">
|
||||
<template #append>
|
||||
<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"
|
||||
:rules="checkNumberRange(0, limits.memory)"
|
||||
>
|
||||
<el-input style="width: 40%" v-model.number="dialogData.rowData!.memoryItem">
|
||||
<template #append><div style="width: 35px">MB</div></template>
|
||||
</el-input>
|
||||
<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;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
if (dialogData.value.title === 'create') {
|
||||
await createContainer(dialogData.value.rowData!)
|
||||
|
@ -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…
x
Reference in New Issue
Block a user