1
0
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:
ssongliu 2023-06-15 20:44:13 +08:00 committed by GitHub
parent aa37e3885c
commit c82e20efd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 170 additions and 52 deletions

View File

@ -205,6 +205,20 @@ func (b *BaseApi) ContainerInfo(c *gin.Context) {
helper.SuccessWithData(c, data) 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 // @Tags Container
// @Summary Create container // @Summary Create container
// @Description 创建容器 // @Description 创建容器

View File

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

View File

@ -27,6 +27,8 @@ import (
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
v1 "github.com/opencontainers/image-spec/specs-go/v1" v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/mem"
) )
type ContainerService struct{} type ContainerService struct{}
@ -42,6 +44,7 @@ type IContainerService interface {
ContainerCreate(req dto.ContainerOperate) error ContainerCreate(req dto.ContainerOperate) error
ContainerUpdate(req dto.ContainerOperate) error ContainerUpdate(req dto.ContainerOperate) error
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error) ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
LoadResouceLimit() (*dto.ResourceLimit, error)
ContainerLogClean(req dto.OperationWithName) error ContainerLogClean(req dto.OperationWithName) error
ContainerOperation(req dto.ContainerOperation) error ContainerOperation(req dto.ContainerOperation) error
ContainerLogs(wsConn *websocket.Conn, container, since, tail string, follow bool) 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 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 { func (u *ContainerService) ContainerCreate(req dto.ContainerOperate) error {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {

View File

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

View File

@ -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": { "/containers/network": {
"post": { "post": {
"security": [ "security": [
@ -12408,6 +12427,17 @@ var doc = `{
} }
} }
}, },
"dto.ResourceLimit": {
"type": "object",
"properties": {
"cpu": {
"type": "integer"
},
"memory": {
"type": "integer"
}
}
},
"dto.SSHConf": { "dto.SSHConf": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -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": { "/containers/network": {
"post": { "post": {
"security": [ "security": [
@ -12394,6 +12413,17 @@
} }
} }
}, },
"dto.ResourceLimit": {
"type": "object",
"properties": {
"cpu": {
"type": "integer"
},
"memory": {
"type": "integer"
}
}
},
"dto.SSHConf": { "dto.SSHConf": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

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

View File

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

View File

@ -5,6 +5,9 @@ import { Container } from '../interface/container';
export const searchContainer = (params: Container.ContainerSearch) => { export const searchContainer = (params: Container.ContainerSearch) => {
return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params, 400000); 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) => { export const createContainer = (params: Container.ContainerHelper) => {
return http.post(`/containers`, params, 3000000); return http.post(`/containers`, params, 3000000);
}; };
@ -17,10 +20,10 @@ export const loadContainerInfo = (name: string) => {
export const cleanContainerLog = (containerName: string) => { export const cleanContainerLog = (containerName: string) => {
return http.post(`/containers/clean/log`, { name: containerName }); 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}`); 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); return http.post(`/containers/operate`, params);
}; };
export const containerPrune = (params: Container.ContainerPrune) => { export const containerPrune = (params: Container.ContainerPrune) => {

View File

@ -507,7 +507,7 @@ const message = {
autoRemove: 'Auto remove', autoRemove: 'Auto remove',
cpuQuota: 'NacosCPU', cpuQuota: 'NacosCPU',
memoryLimit: 'Memory', 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', mount: 'Mount',
serverPath: 'Server path', serverPath: 'Server path',
containerDir: 'Container path', containerDir: 'Container path',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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