Browse Source

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

pull/1584/head
ssongliu 1 year ago committed by GitHub
parent
commit
7b297e824c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      backend/app/api/v1/container.go
  2. 1
      backend/app/dto/container.go
  3. 28
      backend/app/service/container.go
  4. 17
      backend/app/service/container_network.go
  5. 3
      backend/router/ro_container.go
  6. 102
      cmd/server/docs/docs.go
  7. 98
      cmd/server/docs/swagger.json
  8. 61
      cmd/server/docs/swagger.yaml
  9. 1
      frontend/src/api/interface/container.ts
  10. 3
      frontend/src/api/modules/container.ts
  11. 26
      frontend/src/views/container/container/operate/index.vue
  12. 1
      frontend/src/views/container/volume/create/index.vue

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

@ -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
// @Summary Delete network
// @Description 删除容器网络
@ -578,11 +595,10 @@ func (b *BaseApi) SearchVolume(c *gin.Context) {
// @Summary List volumes
// @Description 获取容器存储卷列表
// @Accept json
// @Param request body dto.PageInfo true "request"
// @Produce json
// @Success 200 {object} dto.PageResult
// @Success 200 {array} dto.Options
// @Security ApiKeyAuth
// @Router /containers/volume/search [get]
// @Router /containers/volume [get]
func (b *BaseApi) ListVolume(c *gin.Context) {
list, err := containerService.ListVolume()
if err != nil {

1
backend/app/dto/container.go

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

28
backend/app/service/container.go

@ -37,6 +37,7 @@ type IContainerService interface {
Page(req dto.PageContainer) (int64, interface{}, error)
List() ([]string, error)
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
ListNetwork() ([]dto.Options, error)
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
ListVolume() ([]dto.Options, 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 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
}
@ -320,7 +322,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, &networkConf, &v1.Platform{}, req.Name)
if err != nil {
_ = client.ContainerRemove(ctx, req.Name, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true})
return err
@ -348,6 +350,12 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
data.ContainerID = oldContainer.ID
data.Name = strings.ReplaceAll(oldContainer.Name, "/", "")
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.Env = oldContainer.Config.Env
data.CPUShares = oldContainer.HostConfig.CPUShares
@ -409,7 +417,8 @@ func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
}
config := oldContainer.Config
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
}
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)
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 {
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.Image = req.Image
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 {
return err
}
@ -743,7 +759,7 @@ func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) {
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)
if err != nil {
return err
@ -758,6 +774,8 @@ func loadConfigInfo(req dto.ContainerOperate, config *container.Config, hostConf
config.Labels = stringsToMap(req.Labels)
config.ExposedPorts = exposeds
networkConf.EndpointsConfig = map[string]*network.EndpointSettings{req.Network: {}}
hostConf.AutoRemove = req.AutoRemove
hostConf.CPUShares = req.CPUShares
hostConf.PublishAllPorts = req.PublishAllPorts

17
backend/app/service/container_network.go

@ -75,6 +75,23 @@ func (u *ContainerService) PageNetwork(req dto.SearchWithPage) (int64, interface
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 {
client, err := docker.NewDockerClient()
if err != nil {

3
backend/router/ro_container.go

@ -61,10 +61,11 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
baRouter.POST("/image/tag", baseApi.ImageTag)
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/search", baseApi.SearchNetwork)
baRouter.POST("/network", baseApi.CreateNetwork)
baRouter.GET("/volume", baseApi.ListVolume)
baRouter.POST("/volume/del", baseApi.DeleteVolume)
baRouter.POST("/volume/search", baseApi.SearchVolume)
baRouter.POST("/volume", baseApi.CreateVolume)

102
cmd/server/docs/docs.go

@ -1,5 +1,5 @@
// Package docs GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag
// Code generated by swaggo/swag. DO NOT EDIT.
package docs
import "github.com/swaggo/swag"
@ -1992,6 +1992,35 @@ const docTemplate = `{
}
},
"/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": {
"security": [
{
@ -2864,6 +2893,35 @@ const docTemplate = `{
}
},
"/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": {
"security": [
{
@ -2948,43 +3006,6 @@ const docTemplate = `{
}
},
"/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": {
"security": [
{
@ -11065,6 +11086,9 @@ const docTemplate = `{
"nanoCPUs": {
"type": "integer"
},
"network": {
"type": "string"
},
"publishAllPorts": {
"type": "boolean"
},

98
cmd/server/docs/swagger.json

@ -1985,6 +1985,35 @@
}
},
"/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": {
"security": [
{
@ -2857,6 +2886,35 @@
}
},
"/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": {
"security": [
{
@ -2941,43 +2999,6 @@
}
},
"/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": {
"security": [
{
@ -11058,6 +11079,9 @@
"nanoCPUs": {
"type": "integer"
},
"network": {
"type": "string"
},
"publishAllPorts": {
"type": "boolean"
},

61
cmd/server/docs/swagger.yaml

@ -339,6 +339,8 @@ definitions:
type: string
nanoCPUs:
type: integer
network:
type: string
publishAllPorts:
type: boolean
restartPolicy:
@ -4817,6 +4819,24 @@ paths:
- ApiKeyAuth: []
summary: Load container stats
/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:
consumes:
- application/json
@ -5372,6 +5392,24 @@ paths:
formatZH: 更新容器镜像 [name][image]
paramKeys: []
/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:
consumes:
- application/json
@ -5426,29 +5464,6 @@ paths:
formatZH: 删除容器存储卷 [names]
paramKeys: []
/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:
consumes:
- application/json

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

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

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

@ -75,6 +75,9 @@ export const imageRemove = (params: Container.BatchDelete) => {
export const searchNetwork = (params: SearchWithPage) => {
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) => {
return http.post(`/containers/network/del`, params);
};

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

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

1
frontend/src/views/container/volume/create/index.vue

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

Loading…
Cancel
Save