1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-02-08 01:20:07 +08:00

feat: 容器创建编辑支持自定义网络 (#1582)

This commit is contained in:
ssongliu 2023-07-07 23:17:05 +08:00 committed by GitHub
parent 28bd0e3cc2
commit 7b297e824c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 254 additions and 109 deletions

View File

@ -489,6 +489,23 @@ func (b *BaseApi) SearchNetwork(c *gin.Context) {
}) })
} }
// @Tags Container Network
// @Summary List networks
// @Description 获取容器网络列表
// @Accept json
// @Produce json
// @Success 200 {array} dto.Options
// @Security ApiKeyAuth
// @Router /containers/network [get]
func (b *BaseApi) ListNetwork(c *gin.Context) {
list, err := containerService.ListNetwork()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, list)
}
// @Tags Container Network // @Tags Container Network
// @Summary Delete network // @Summary Delete network
// @Description 删除容器网络 // @Description 删除容器网络
@ -578,11 +595,10 @@ func (b *BaseApi) SearchVolume(c *gin.Context) {
// @Summary List volumes // @Summary List volumes
// @Description 获取容器存储卷列表 // @Description 获取容器存储卷列表
// @Accept json // @Accept json
// @Param request body dto.PageInfo true "request"
// @Produce json // @Produce json
// @Success 200 {object} dto.PageResult // @Success 200 {array} dto.Options
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /containers/volume/search [get] // @Router /containers/volume [get]
func (b *BaseApi) ListVolume(c *gin.Context) { func (b *BaseApi) ListVolume(c *gin.Context) {
list, err := containerService.ListVolume() list, err := containerService.ListVolume()
if err != nil { if err != nil {

View File

@ -39,6 +39,7 @@ type ContainerOperate struct {
ContainerID string `json:"containerID"` ContainerID string `json:"containerID"`
Name string `json:"name"` Name string `json:"name"`
Image string `json:"image"` Image string `json:"image"`
Network string `json:"network"`
PublishAllPorts bool `json:"publishAllPorts"` PublishAllPorts bool `json:"publishAllPorts"`
ExposedPorts []PortHelper `json:"exposedPorts"` ExposedPorts []PortHelper `json:"exposedPorts"`
Cmd []string `json:"cmd"` Cmd []string `json:"cmd"`

View File

@ -37,6 +37,7 @@ type IContainerService interface {
Page(req dto.PageContainer) (int64, interface{}, error) Page(req dto.PageContainer) (int64, interface{}, error)
List() ([]string, error) List() ([]string, error)
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
ListNetwork() ([]dto.Options, error)
PageVolume(req dto.SearchWithPage) (int64, interface{}, error) PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
ListVolume() ([]dto.Options, error) ListVolume() ([]dto.Options, error)
PageCompose(req dto.SearchWithPage) (int64, interface{}, error) PageCompose(req dto.SearchWithPage) (int64, interface{}, error)
@ -309,7 +310,8 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate) error {
var config container.Config var config container.Config
var hostConf container.HostConfig var hostConf container.HostConfig
if err := loadConfigInfo(req, &config, &hostConf); err != nil { var networkConf network.NetworkingConfig
if err := loadConfigInfo(req, &config, &hostConf, &networkConf); err != nil {
return err return err
} }
@ -320,7 +322,7 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate) error {
return err return err
} }
} }
container, err := client.ContainerCreate(ctx, &config, &hostConf, &network.NetworkingConfig{}, &v1.Platform{}, req.Name) container, err := client.ContainerCreate(ctx, &config, &hostConf, &networkConf, &v1.Platform{}, req.Name)
if err != nil { if err != nil {
_ = client.ContainerRemove(ctx, req.Name, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true}) _ = client.ContainerRemove(ctx, req.Name, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true})
return err return err
@ -348,6 +350,12 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
data.ContainerID = oldContainer.ID data.ContainerID = oldContainer.ID
data.Name = strings.ReplaceAll(oldContainer.Name, "/", "") data.Name = strings.ReplaceAll(oldContainer.Name, "/", "")
data.Image = oldContainer.Config.Image data.Image = oldContainer.Config.Image
if oldContainer.NetworkSettings != nil {
for network := range oldContainer.NetworkSettings.Networks {
data.Network = network
break
}
}
data.Cmd = oldContainer.Config.Cmd data.Cmd = oldContainer.Config.Cmd
data.Env = oldContainer.Config.Env data.Env = oldContainer.Config.Env
data.CPUShares = oldContainer.HostConfig.CPUShares data.CPUShares = oldContainer.HostConfig.CPUShares
@ -409,7 +417,8 @@ func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
} }
config := oldContainer.Config config := oldContainer.Config
hostConf := oldContainer.HostConfig hostConf := oldContainer.HostConfig
if err := loadConfigInfo(req, config, hostConf); err != nil { var networkConf network.NetworkingConfig
if err := loadConfigInfo(req, config, hostConf, &networkConf); err != nil {
return err return err
} }
if err := client.ContainerRemove(ctx, req.ContainerID, types.ContainerRemoveOptions{Force: true}); err != nil { if err := client.ContainerRemove(ctx, req.ContainerID, types.ContainerRemoveOptions{Force: true}); err != nil {
@ -417,7 +426,7 @@ func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
} }
global.LOG.Infof("new container info %s has been update, now start to recreate", req.Name) 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) container, err := client.ContainerCreate(ctx, config, hostConf, &networkConf, &v1.Platform{}, req.Name)
if err != nil { if err != nil {
return fmt.Errorf("recreate contianer failed, err: %v", err) return fmt.Errorf("recreate contianer failed, err: %v", err)
} }
@ -447,6 +456,13 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
config := oldContainer.Config config := oldContainer.Config
config.Image = req.Image config.Image = req.Image
hostConf := oldContainer.HostConfig hostConf := oldContainer.HostConfig
var networkConf network.NetworkingConfig
if oldContainer.NetworkSettings != nil {
for networkKey := range oldContainer.NetworkSettings.Networks {
networkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
break
}
}
if err := client.ContainerRemove(ctx, req.Name, types.ContainerRemoveOptions{Force: true}); err != nil { if err := client.ContainerRemove(ctx, req.Name, types.ContainerRemoveOptions{Force: true}); err != nil {
return err return err
} }
@ -743,7 +759,7 @@ func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) {
return portMap, nil return portMap, nil
} }
func loadConfigInfo(req dto.ContainerOperate, config *container.Config, hostConf *container.HostConfig) error { func loadConfigInfo(req dto.ContainerOperate, config *container.Config, hostConf *container.HostConfig, networkConf *network.NetworkingConfig) error {
portMap, err := checkPortStats(req.ExposedPorts) portMap, err := checkPortStats(req.ExposedPorts)
if err != nil { if err != nil {
return err return err
@ -758,6 +774,8 @@ func loadConfigInfo(req dto.ContainerOperate, config *container.Config, hostConf
config.Labels = stringsToMap(req.Labels) config.Labels = stringsToMap(req.Labels)
config.ExposedPorts = exposeds config.ExposedPorts = exposeds
networkConf.EndpointsConfig = map[string]*network.EndpointSettings{req.Network: {}}
hostConf.AutoRemove = req.AutoRemove hostConf.AutoRemove = req.AutoRemove
hostConf.CPUShares = req.CPUShares hostConf.CPUShares = req.CPUShares
hostConf.PublishAllPorts = req.PublishAllPorts hostConf.PublishAllPorts = req.PublishAllPorts

View File

@ -75,6 +75,23 @@ func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface
return int64(total), data, nil return int64(total), data, nil
} }
func (u *ContainerService) ListNetwork() ([]dto.Options, error) {
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
list, err := client.NetworkList(context.TODO(), types.NetworkListOptions{})
if err != nil {
return nil, err
}
var datas []dto.Options
for _, item := range list {
datas = append(datas, dto.Options{Option: item.Name})
}
return datas, nil
}
func (u *ContainerService) DeleteNetwork(req dto.BatchDelete) error { func (u *ContainerService) DeleteNetwork(req dto.BatchDelete) error {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {

View File

@ -61,10 +61,11 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
baRouter.POST("/image/tag", baseApi.ImageTag) baRouter.POST("/image/tag", baseApi.ImageTag)
baRouter.POST("/image/build", baseApi.ImageBuild) baRouter.POST("/image/build", baseApi.ImageBuild)
baRouter.GET("/volume", baseApi.ListVolume) baRouter.GET("/network", baseApi.ListNetwork)
baRouter.POST("/network/del", baseApi.DeleteNetwork) baRouter.POST("/network/del", baseApi.DeleteNetwork)
baRouter.POST("/network/search", baseApi.SearchNetwork) baRouter.POST("/network/search", baseApi.SearchNetwork)
baRouter.POST("/network", baseApi.CreateNetwork) baRouter.POST("/network", baseApi.CreateNetwork)
baRouter.GET("/volume", baseApi.ListVolume)
baRouter.POST("/volume/del", baseApi.DeleteVolume) baRouter.POST("/volume/del", baseApi.DeleteVolume)
baRouter.POST("/volume/search", baseApi.SearchVolume) baRouter.POST("/volume/search", baseApi.SearchVolume)
baRouter.POST("/volume", baseApi.CreateVolume) baRouter.POST("/volume", baseApi.CreateVolume)

View File

@ -1,5 +1,5 @@
// Package docs GENERATED BY SWAG; DO NOT EDIT // Code generated by swaggo/swag. DO NOT EDIT.
// This file was generated by swaggo/swag
package docs package docs
import "github.com/swaggo/swag" import "github.com/swaggo/swag"
@ -1992,6 +1992,35 @@ const docTemplate = `{
} }
}, },
"/containers/network": { "/containers/network": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取容器网络列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Container Network"
],
"summary": "List networks",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.Options"
}
}
}
}
},
"post": { "post": {
"security": [ "security": [
{ {
@ -2864,6 +2893,35 @@ const docTemplate = `{
} }
}, },
"/containers/volume": { "/containers/volume": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取容器存储卷列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Container Volume"
],
"summary": "List volumes",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.Options"
}
}
}
}
},
"post": { "post": {
"security": [ "security": [
{ {
@ -2948,43 +3006,6 @@ const docTemplate = `{
} }
}, },
"/containers/volume/search": { "/containers/volume/search": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取容器存储卷列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Container Volume"
],
"summary": "List volumes",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.PageInfo"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
},
"post": { "post": {
"security": [ "security": [
{ {
@ -11065,6 +11086,9 @@ const docTemplate = `{
"nanoCPUs": { "nanoCPUs": {
"type": "integer" "type": "integer"
}, },
"network": {
"type": "string"
},
"publishAllPorts": { "publishAllPorts": {
"type": "boolean" "type": "boolean"
}, },

View File

@ -1985,6 +1985,35 @@
} }
}, },
"/containers/network": { "/containers/network": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取容器网络列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Container Network"
],
"summary": "List networks",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.Options"
}
}
}
}
},
"post": { "post": {
"security": [ "security": [
{ {
@ -2857,6 +2886,35 @@
} }
}, },
"/containers/volume": { "/containers/volume": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取容器存储卷列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Container Volume"
],
"summary": "List volumes",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.Options"
}
}
}
}
},
"post": { "post": {
"security": [ "security": [
{ {
@ -2941,43 +2999,6 @@
} }
}, },
"/containers/volume/search": { "/containers/volume/search": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取容器存储卷列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Container Volume"
],
"summary": "List volumes",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.PageInfo"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
},
"post": { "post": {
"security": [ "security": [
{ {
@ -11058,6 +11079,9 @@
"nanoCPUs": { "nanoCPUs": {
"type": "integer" "type": "integer"
}, },
"network": {
"type": "string"
},
"publishAllPorts": { "publishAllPorts": {
"type": "boolean" "type": "boolean"
}, },

View File

@ -339,6 +339,8 @@ definitions:
type: string type: string
nanoCPUs: nanoCPUs:
type: integer type: integer
network:
type: string
publishAllPorts: publishAllPorts:
type: boolean type: boolean
restartPolicy: restartPolicy:
@ -4817,6 +4819,24 @@ paths:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Load container stats summary: Load container stats
/containers/network: /containers/network:
get:
consumes:
- application/json
description: 获取容器网络列表
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/dto.Options'
type: array
security:
- ApiKeyAuth: []
summary: List networks
tags:
- Container Network
post: post:
consumes: consumes:
- application/json - application/json
@ -5372,6 +5392,24 @@ paths:
formatZH: 更新容器镜像 [name][image] formatZH: 更新容器镜像 [name][image]
paramKeys: [] paramKeys: []
/containers/volume: /containers/volume:
get:
consumes:
- application/json
description: 获取容器存储卷列表
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/dto.Options'
type: array
security:
- ApiKeyAuth: []
summary: List volumes
tags:
- Container Volume
post: post:
consumes: consumes:
- application/json - application/json
@ -5426,29 +5464,6 @@ paths:
formatZH: 删除容器存储卷 [names] formatZH: 删除容器存储卷 [names]
paramKeys: [] paramKeys: []
/containers/volume/search: /containers/volume/search:
get:
consumes:
- application/json
description: 获取容器存储卷列表
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.PageInfo'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.PageResult'
security:
- ApiKeyAuth: []
summary: List volumes
tags:
- Container Volume
post: post:
consumes: consumes:
- application/json - application/json

View File

@ -20,6 +20,7 @@ export namespace Container {
containerID: string; containerID: string;
name: string; name: string;
image: string; image: string;
network: string;
cmdStr: string; cmdStr: string;
memoryItem: number; memoryItem: number;
cmd: Array<string>; cmd: Array<string>;

View File

@ -75,6 +75,9 @@ export const imageRemove = (params: Container.BatchDelete) => {
export const searchNetwork = (params: SearchWithPage) => { export const searchNetwork = (params: SearchWithPage) => {
return http.post<ResPage<Container.NetworkInfo>>(`/containers/network/search`, params); return http.post<ResPage<Container.NetworkInfo>>(`/containers/network/search`, params);
}; };
export const listNetwork = () => {
return http.get<Array<Container.Options>>(`/containers/network`);
};
export const deleteNetwork = (params: Container.BatchDelete) => { export const deleteNetwork = (params: Container.BatchDelete) => {
return http.post(`/containers/network/del`, params); return http.post(`/containers/network/del`, params);
}; };

View File

@ -82,6 +82,16 @@
</table> </table>
</el-card> </el-card>
</el-form-item> </el-form-item>
<el-form-item :label="$t('container.network')" prop="network">
<el-select v-model="dialogData.rowData!.network">
<el-option
v-for="(item, indexV) of networks"
:key="indexV"
:value="item.option"
:label="item.option"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('container.cmd')" prop="cmdStr"> <el-form-item :label="$t('container.cmd')" prop="cmdStr">
<el-input :placeholder="$t('container.cmdHelper')" v-model="dialogData.rowData!.cmdStr" /> <el-input :placeholder="$t('container.cmdHelper')" v-model="dialogData.rowData!.cmdStr" />
</el-form-item> </el-form-item>
@ -222,7 +232,14 @@ 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, loadResourceLimit } from '@/api/modules/container'; import {
listImage,
listVolume,
createContainer,
updateContainer,
loadResourceLimit,
listNetwork,
} 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 { checkIpV4V6, checkPort } from '@/utils/util'; import { checkIpV4V6, checkPort } from '@/utils/util';
@ -263,12 +280,14 @@ const acceptParams = (params: DialogProps): void => {
loadLimit(); loadLimit();
loadImageOptions(); loadImageOptions();
loadVolumeOptions(); loadVolumeOptions();
loadNetworkOptions();
drawerVisiable.value = true; drawerVisiable.value = true;
}; };
const emit = defineEmits<{ (e: 'search'): void }>(); const emit = defineEmits<{ (e: 'search'): void }>();
const images = ref(); const images = ref();
const volumes = ref(); const volumes = ref();
const networks = ref();
const limits = ref<Container.ResourceLimit>({ const limits = ref<Container.ResourceLimit>({
cpu: null as number, cpu: null as number,
memory: null as number, memory: null as number,
@ -279,6 +298,7 @@ const handleClose = () => {
}; };
const rules = reactive({ const rules = reactive({
network: [Rules.requiredSelect],
cpuShares: [Rules.number, checkNumberRange(0, 262144)], cpuShares: [Rules.number, checkNumberRange(0, 262144)],
name: [Rules.requiredInput, Rules.name], name: [Rules.requiredInput, Rules.name],
image: [Rules.requiredSelect], image: [Rules.requiredSelect],
@ -329,6 +349,10 @@ const loadVolumeOptions = async () => {
const res = await listVolume(); const res = await listVolume();
volumes.value = res.data; volumes.value = res.data;
}; };
const loadNetworkOptions = async () => {
const res = await listNetwork();
networks.value = res.data;
};
const onSubmit = async (formEl: FormInstance | undefined) => { const onSubmit = async (formEl: FormInstance | undefined) => {
if (dialogData.value.rowData!.volumes.length !== 0) { if (dialogData.value.rowData!.volumes.length !== 0) {
for (const item of dialogData.value.rowData!.volumes) { for (const item of dialogData.value.rowData!.volumes) {

View File

@ -12,6 +12,7 @@
:model="form" :model="form"
:rules="rules" :rules="rules"
label-width="80px" label-width="80px"
@submit.prevent
> >
<el-form-item :label="$t('commons.table.name')" prop="name"> <el-form-item :label="$t('commons.table.name')" prop="name">
<el-input clearable v-model.trim="form.name" /> <el-input clearable v-model.trim="form.name" />