feat: 实现容器网络功能

This commit is contained in:
ssongliu 2022-10-11 14:20:51 +08:00 committed by ssongliu
parent f98f9a5872
commit 1d2a00cc6e
17 changed files with 1518 additions and 23 deletions

View File

@ -80,3 +80,115 @@ func (b *BaseApi) ContainerLogs(c *gin.Context) {
} }
helper.SuccessWithData(c, logs) helper.SuccessWithData(c, logs)
} }
func (b *BaseApi) SearchNetwork(c *gin.Context) {
var req dto.PageInfo
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
total, list, err := containerService.PageNetwork(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
func (b *BaseApi) DeleteNetwork(c *gin.Context) {
var req dto.BatchDelete
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := containerService.DeleteNetwork(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) CreateNetwork(c *gin.Context) {
var req dto.NetworkCreat
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := containerService.CreateNetwork(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) SearchVolume(c *gin.Context) {
var req dto.PageInfo
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
total, list, err := containerService.PageVolume(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
func (b *BaseApi) DeleteVolume(c *gin.Context) {
var req dto.BatchDelete
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := containerService.DeleteVolume(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) CreateVolume(c *gin.Context) {
var req dto.VolumeCreat
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := containerService.CreateVolume(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

View File

@ -31,6 +31,25 @@ func (b *BaseApi) SearchImage(c *gin.Context) {
}) })
} }
func (b *BaseApi) ImageBuild(c *gin.Context) {
var req dto.ImageBuild
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := imageService.ImageBuild(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) ImagePull(c *gin.Context) { func (b *BaseApi) ImagePull(c *gin.Context) {
var req dto.ImagePull var req dto.ImagePull
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {

View File

@ -1,5 +1,7 @@
package dto package dto
import "time"
type PageContainer struct { type PageContainer struct {
PageInfo PageInfo
Status string `json:"status" validate:"required,oneof=all running"` Status string `json:"status" validate:"required,oneof=all running"`
@ -25,3 +27,44 @@ type ContainerOperation struct {
Operation string `json:"operation" validate:"required,oneof=start stop reStart kill pause unPause reName remove"` Operation string `json:"operation" validate:"required,oneof=start stop reStart kill pause unPause reName remove"`
NewName string `json:"newName"` NewName string `json:"newName"`
} }
type Network struct {
ID string `json:"id"`
Name string `json:"name"`
Labels []string `json:"labels"`
Driver string `json:"driver"`
IPAMDriver string `json:"ipamDriver"`
IPV4Subnet string `json:"ipv4Subnet"`
IPV4Gateway string `json:"ipv4Gateway"`
IPV6Subnet string `json:"ipv6Subnet"`
IPV6Gateway string `json:"ipv6Gateway"`
CreatedAt time.Time `json:"createdAt"`
Attachable bool `json:"attachable"`
}
type NetworkCreat struct {
Name string `json:"name"`
Driver string `json:"driver"`
Options []string `json:"options"`
IPV4Subnet string `json:"ipv4Subnet"`
IPV4Gateway string `json:"ipv4Gateway"`
Scope string `json:"scope"`
Labels []string `json:"labels"`
}
type Volume struct {
Name string `json:"name"`
Labels []string `json:"labels"`
Driver string `json:"driver"`
Mountpoint string `json:"mountpoint"`
CreatedAt time.Time `json:"createdAt"`
}
type VolumeCreat struct {
Name string `json:"name"`
Driver string `json:"driver"`
Options []string `json:"options"`
Labels []string `json:"labels"`
}
type BatchDelete struct {
Ids []string `json:"ids" validate:"required"`
}

View File

@ -18,6 +18,12 @@ type ImageRemove struct {
ImageName string `josn:"imageName" validate:"required"` ImageName string `josn:"imageName" validate:"required"`
} }
type ImageBuild struct {
From string `josn:"from" validate:"required"`
Dockerfile string `josn:"dockerfile" validate:"required"`
Tags string `josn:"tags" validate:"required"`
}
type ImagePull struct { type ImagePull struct {
RepoID uint `josn:"repoID"` RepoID uint `josn:"repoID"`
ImageName string `josn:"imageName" validate:"required"` ImageName string `josn:"imageName" validate:"required"`

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"strings" "strings"
"time" "time"
@ -12,6 +13,9 @@ import (
"github.com/1Panel-dev/1Panel/constant" "github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/utils/docker" "github.com/1Panel-dev/1Panel/utils/docker"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/stdcopy"
) )
@ -19,14 +23,21 @@ type ContainerService struct{}
type IContainerService interface { type IContainerService interface {
Page(req dto.PageContainer) (int64, interface{}, error) Page(req dto.PageContainer) (int64, interface{}, error)
PageNetwork(req dto.PageInfo) (int64, interface{}, error)
PageVolume(req dto.PageInfo) (int64, interface{}, error)
ContainerOperation(req dto.ContainerOperation) error ContainerOperation(req dto.ContainerOperation) error
ContainerLogs(param dto.ContainerLog) (string, error) ContainerLogs(param dto.ContainerLog) (string, error)
ContainerInspect(id string) (string, error) ContainerInspect(id string) (string, error)
DeleteNetwork(req dto.BatchDelete) error
CreateNetwork(req dto.NetworkCreat) error
DeleteVolume(req dto.BatchDelete) error
CreateVolume(req dto.VolumeCreat) error
} }
func NewIContainerService() IContainerService { func NewIContainerService() IContainerService {
return &ContainerService{} return &ContainerService{}
} }
func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) { func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) {
var ( var (
records []types.Container records []types.Container
@ -138,3 +149,174 @@ func (u *ContainerService) ContainerLogs(req dto.ContainerLog) (string, error) {
} }
return buf.String(), nil return buf.String(), nil
} }
func (u *ContainerService) PageNetwork(req dto.PageInfo) (int64, interface{}, error) {
client, err := docker.NewDockerClient()
if err != nil {
return 0, nil, err
}
list, err := client.NetworkList(context.TODO(), types.NetworkListOptions{})
if err != nil {
return 0, nil, err
}
var (
data []dto.Network
records []types.NetworkResource
)
total, start, end := len(list), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
records = make([]types.NetworkResource, 0)
} else {
if end >= total {
end = total
}
records = list[start:end]
}
for _, item := range records {
tag := make([]string, 0)
for key, val := range item.Labels {
tag = append(tag, fmt.Sprintf("%s=%s", key, val))
}
var (
ipv4 network.IPAMConfig
ipv6 network.IPAMConfig
)
if len(item.IPAM.Config) > 1 {
ipv4 = item.IPAM.Config[0]
ipv6 = item.IPAM.Config[1]
} else if len(item.IPAM.Config) > 0 {
ipv4 = item.IPAM.Config[0]
}
data = append(data, dto.Network{
ID: item.ID,
CreatedAt: item.Created,
Name: item.Name,
Driver: item.Driver,
IPAMDriver: item.IPAM.Driver,
IPV4Subnet: ipv4.Subnet,
IPV4Gateway: ipv4.Gateway,
IPV6Subnet: ipv6.Subnet,
IPV6Gateway: ipv6.Gateway,
Attachable: item.Attachable,
Labels: tag,
})
}
return int64(total), data, nil
}
func (u *ContainerService) DeleteNetwork(req dto.BatchDelete) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
for _, id := range req.Ids {
if err := client.NetworkRemove(context.TODO(), id); err != nil {
return err
}
}
return nil
}
func (u *ContainerService) CreateNetwork(req dto.NetworkCreat) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
ipv4 := network.IPAMConfig{
Subnet: req.IPV4Subnet,
Gateway: req.IPV4Gateway,
}
options := types.NetworkCreate{
Driver: req.Driver,
Scope: req.Scope,
IPAM: &network.IPAM{
Config: []network.IPAMConfig{ipv4},
},
Options: stringsToMap(req.Options),
Labels: stringsToMap(req.Labels),
}
if _, err := client.NetworkCreate(context.TODO(), req.Name, options); err != nil {
return err
}
return nil
}
func (u *ContainerService) PageVolume(req dto.PageInfo) (int64, interface{}, error) {
client, err := docker.NewDockerClient()
if err != nil {
return 0, nil, err
}
list, err := client.VolumeList(context.TODO(), filters.NewArgs())
if err != nil {
return 0, nil, err
}
var (
data []dto.Volume
records []*types.Volume
)
total, start, end := len(list.Volumes), (req.Page-1)*req.PageSize, req.Page*req.PageSize
if start > total {
records = make([]*types.Volume, 0)
} else {
if end >= total {
end = total
}
records = list.Volumes[start:end]
}
for _, item := range records {
tag := make([]string, 0)
for _, val := range item.Labels {
tag = append(tag, val)
}
createTime, _ := time.Parse("2006-01-02T15:04:05Z", item.CreatedAt)
data = append(data, dto.Volume{
CreatedAt: createTime,
Name: item.Name,
Driver: item.Driver,
Mountpoint: item.Mountpoint,
Labels: tag,
})
}
return int64(total), data, nil
}
func (u *ContainerService) DeleteVolume(req dto.BatchDelete) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
for _, id := range req.Ids {
if err := client.VolumeRemove(context.TODO(), id, true); err != nil {
return err
}
}
return nil
}
func (u *ContainerService) CreateVolume(req dto.VolumeCreat) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
options := volume.VolumeCreateBody{
Name: req.Name,
Driver: req.Driver,
DriverOpts: stringsToMap(req.Options),
Labels: stringsToMap(req.Labels),
}
if _, err := client.VolumeCreate(context.TODO(), options); err != nil {
return err
}
return nil
}
func stringsToMap(list []string) map[string]string {
var lableMap = make(map[string]string)
for _, label := range list {
sps := strings.Split(label, "=")
if len(sps) > 1 {
lableMap[sps[0]] = sps[1]
}
}
return lableMap
}

View File

@ -73,6 +73,29 @@ func (u *ImageService) Page(req dto.PageInfo) (int64, interface{}, error) {
return int64(total), backDatas, nil return int64(total), backDatas, nil
} }
func (u *ImageService) ImageBuild(req dto.ImageBuild) error {
// client, err := docker.NewDockerClient()
// if err != nil {
// return err
// }
// if req.From == "path" {
// tar, err := archive.TarWithOptions("node-hello/", &archive.TarOptions{})
// if err != nil {
// return err
// }
// opts := types.ImageBuildOptions{
// Dockerfile: "Dockerfile",
// Tags: []string{dockerRegistryUserID + "/node-hello"},
// Remove: true,
// }
// if _, err := client.ImageBuild(context.TODO(), tar, opts); err != nil {
// return err
// }
// }
return nil
}
func (u *ImageService) ImagePull(req dto.ImagePull) error { func (u *ImageService) ImagePull(req dto.ImagePull) error {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
@ -185,8 +208,10 @@ func (u *ImageService) ImagePush(req dto.ImagePush) error {
options.RegistryAuth = authStr options.RegistryAuth = authStr
} }
newName := fmt.Sprintf("%s/%s", repo.DownloadUrl, req.TagName) newName := fmt.Sprintf("%s/%s", repo.DownloadUrl, req.TagName)
if err := client.ImageTag(context.TODO(), req.ImageName, newName); err != nil { if newName != req.ImageName {
return err if err := client.ImageTag(context.TODO(), req.ImageName, newName); err != nil {
return err
}
} }
go func() { go func() {
out, err := client.ImagePush(context.TODO(), newName, options) out, err := client.ImagePush(context.TODO(), newName, options)

View File

@ -8,9 +8,14 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
"time"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant" "github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/utils/docker" "github.com/1Panel-dev/1Panel/utils/docker"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/archive"
) )
func TestImage(t *testing.T) { func TestImage(t *testing.T) {
@ -32,6 +37,28 @@ func TestImage(t *testing.T) {
} }
} }
func TestBuild(t *testing.T) {
client, err := docker.NewDockerClient()
if err != nil {
fmt.Println(err)
}
tar, err := archive.TarWithOptions("/Users/slooop/Documents/neeko/", &archive.TarOptions{})
if err != nil {
fmt.Println(err)
}
opts := types.ImageBuildOptions{
Dockerfile: "Dockerfile",
Tags: []string{"neeko" + "/test"},
Remove: true,
}
res, err := client.ImageBuild(context.TODO(), tar, opts)
if err != nil {
fmt.Println(err)
}
defer res.Body.Close()
}
func TestDeam(t *testing.T) { func TestDeam(t *testing.T) {
file, err := ioutil.ReadFile(constant.DaemonJsonDir) file, err := ioutil.ReadFile(constant.DaemonJsonDir)
if err != nil { if err != nil {
@ -62,3 +89,30 @@ func TestDeam(t *testing.T) {
fmt.Println(err) fmt.Println(err)
} }
} }
func TestNetwork(t *testing.T) {
client, err := docker.NewDockerClient()
if err != nil {
fmt.Println(err)
}
var data []dto.Volume
list, err := client.VolumeList(context.TODO(), filters.NewArgs())
if err != nil {
fmt.Println(err)
}
for _, item := range list.Volumes {
tag := make([]string, 0)
for _, val := range item.Labels {
tag = append(tag, val)
}
createTime, _ := time.Parse("2006-01-02T15:04:05Z", item.CreatedAt)
data = append(data, dto.Volume{
CreatedAt: createTime,
Name: item.Name,
Driver: item.Driver,
Mountpoint: item.Mountpoint,
Labels: tag,
})
}
fmt.Println(data)
}

View File

@ -37,5 +37,12 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
baRouter.POST("/image/save", baseApi.ImageSave) baRouter.POST("/image/save", baseApi.ImageSave)
baRouter.POST("/image/load", baseApi.ImageLoad) baRouter.POST("/image/load", baseApi.ImageLoad)
baRouter.POST("/image/remove", baseApi.ImageRemove) baRouter.POST("/image/remove", baseApi.ImageRemove)
baRouter.POST("/network/del", baseApi.DeleteNetwork)
baRouter.POST("/network/search", baseApi.SearchNetwork)
baRouter.POST("/network", baseApi.CreateNetwork)
baRouter.POST("/volume/del", baseApi.DeleteVolume)
baRouter.POST("/volume/search", baseApi.SearchVolume)
baRouter.POST("/volume", baseApi.CreateVolume)
} }
} }

View File

@ -1,14 +1,9 @@
import { ReqPage } from '.';
export namespace Container { export namespace Container {
export interface ContainerOperate { export interface ContainerOperate {
containerID: string; containerID: string;
operation: string; operation: string;
newName: string; newName: string;
} }
export interface ContainerSearch extends ReqPage {
status: string;
}
export interface ContainerInfo { export interface ContainerInfo {
containerID: string; containerID: string;
name: string; name: string;
@ -29,6 +24,10 @@ export namespace Container {
version: string; version: string;
size: string; size: string;
} }
export interface ImageBuild {
from: string;
dockerfile: string;
}
export interface ImagePull { export interface ImagePull {
repoID: number; repoID: number;
imageName: string; imageName: string;
@ -50,6 +49,43 @@ export namespace Container {
name: string; name: string;
} }
export interface NetworkInfo {
id: string;
name: string;
labels: Array<string>;
driver: string;
ipamDriver: string;
ipv4Subnet: string;
ipv4Gateway: string;
ipv6Subnet: string;
ipv6Gateway: string;
createdAt: string;
attachable: string;
}
export interface NetworkCreate {
name: string;
labels: Array<string>;
options: Array<string>;
driver: string;
ipv4Subnet: string;
ipv4Gateway: string;
scope: string;
}
export interface VolumeInfo {
name: string;
labels: Array<string>;
driver: string;
mountpoint: string;
createdAt: string;
}
export interface VolumeCreate {
name: string;
driver: string;
options: Array<string>;
label: Array<string>;
}
export interface RepoCreate { export interface RepoCreate {
name: string; name: string;
downloadUrl: string; downloadUrl: string;
@ -81,4 +117,8 @@ export namespace Container {
name: string; name: string;
downloadUrl: string; downloadUrl: string;
} }
export interface BatchDelete {
ids: Array<string>;
}
} }

View File

@ -2,7 +2,7 @@ import http from '@/api';
import { ResPage, ReqPage } from '../interface'; import { ResPage, ReqPage } from '../interface';
import { Container } from '../interface/container'; import { Container } from '../interface/container';
export const getContainerPage = (params: Container.ContainerSearch) => { export const getContainerPage = (params: ReqPage) => {
return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params); return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params);
}; };
@ -22,6 +22,9 @@ export const getContainerInspect = (containerID: string) => {
export const getImagePage = (params: ReqPage) => { export const getImagePage = (params: ReqPage) => {
return http.post<ResPage<Container.ImageInfo>>(`/containers/image/search`, params); return http.post<ResPage<Container.ImageInfo>>(`/containers/image/search`, params);
}; };
export const imageBuild = (params: Container.ImageBuild) => {
return http.post<string>(`/containers/image/build`, params);
};
export const imagePull = (params: Container.ImagePull) => { export const imagePull = (params: Container.ImagePull) => {
return http.post<string>(`/containers/image/pull`, params); return http.post<string>(`/containers/image/pull`, params);
}; };
@ -38,6 +41,28 @@ export const imageRemove = (params: Container.ImageRemove) => {
return http.post(`/containers/image/remove`, params); return http.post(`/containers/image/remove`, params);
}; };
// network
export const getNetworkPage = (params: ReqPage) => {
return http.post<ResPage<Container.NetworkInfo>>(`/containers/network/search`, params);
};
export const deleteNetwork = (params: Container.BatchDelete) => {
return http.post(`/containers/network/del`, params);
};
export const createNetwork = (params: Container.NetworkCreate) => {
return http.post(`/containers/network`, params);
};
// volume
export const getVolumePage = (params: ReqPage) => {
return http.post<ResPage<Container.VolumeInfo>>(`/containers/volume/search`, params);
};
export const deleteVolume = (params: Container.BatchDelete) => {
return http.post(`/containers/volume/del`, params);
};
export const createVolume = (params: Container.VolumeCreate) => {
return http.post(`/containers/volume`, params);
};
// repo // repo
export const getRepoPage = (params: ReqPage) => { export const getRepoPage = (params: ReqPage) => {
return http.post<ResPage<Container.RepoInfo>>(`/containers/repo/search`, params); return http.post<ResPage<Container.RepoInfo>>(`/containers/repo/search`, params);

View File

@ -158,7 +158,6 @@ export default {
reName: '重命名', reName: '重命名',
remove: '移除', remove: '移除',
container: '容器', container: '容器',
network: '网络',
storage: '数据卷', storage: '数据卷',
schedule: '编排', schedule: '编排',
upTime: '运行时长', upTime: '运行时长',
@ -178,6 +177,8 @@ export default {
importImage: '导入镜像', importImage: '导入镜像',
import: '导入', import: '导入',
build: '构建镜像', build: '构建镜像',
edit: '编辑',
pathSelect: '路径选择',
label: '标签', label: '标签',
push: '推送', push: '推送',
fileName: '文件名', fileName: '文件名',
@ -186,6 +187,17 @@ export default {
version: '版本', version: '版本',
size: '大小', size: '大小',
from: '来源', from: '来源',
tag: '标签',
network: '网络',
createNetwork: '添加网络',
networkName: '网络名',
driver: '模式',
option: '参数',
attachable: '可用',
subnet: '子网',
scope: 'IP 范围',
gateway: '网关',
repo: '仓库', repo: '仓库',
name: '名称', name: '名称',

View File

@ -9,7 +9,7 @@
<el-button @click="onOpenload"> <el-button @click="onOpenload">
{{ $t('container.importImage') }} {{ $t('container.importImage') }}
</el-button> </el-button>
<el-button @click="onBatchDelete(null)"> <el-button @click="onOpenBuild">
{{ $t('container.build') }} {{ $t('container.build') }}
</el-button> </el-button>
<el-button type="danger" plain :disabled="selects.length === 0" @click="onBatchDelete(null)"> <el-button type="danger" plain :disabled="selects.length === 0" @click="onBatchDelete(null)">
@ -30,6 +30,41 @@
</ComplexTable> </ComplexTable>
</el-card> </el-card>
<el-dialog v-model="buildVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="50%">
<template #header>
<div class="card-header">
<span>{{ $t('container.importImage') }}</span>
</div>
</template>
<el-form ref="buildFormRef" :model="buildForm" label-position="left" label-width="80px">
<el-form-item label="Dockerfile" :rules="Rules.requiredSelect" prop="from">
<el-radio-group v-model="buildForm.from">
<el-radio label="edit">{{ $t('container.edit') }}</el-radio>
<el-radio label="path">{{ $t('container.pathSelect') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="buildForm.from !== 'edit'" :rules="Rules.requiredInput">
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 10 }" v-model="buildForm.dockerfile" />
</el-form-item>
<el-form-item v-else :rules="Rules.requiredInput">
<el-input clearable v-model="buildForm.dockerfile">
<template #append>
<FileList @choose="loadBuildDir" :dir="true"></FileList>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('container.tag')">
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" v-model="buildForm.tag" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="submitBuild(buildFormRef)">{{ $t('container.import') }}</el-button>
<el-button @click="buildVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="pullVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%"> <el-dialog v-model="pullVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
@ -164,6 +199,7 @@ import { Container } from '@/api/interface/container';
import { import {
getImagePage, getImagePage,
getRepoOption, getRepoOption,
imageBuild,
imageLoad, imageLoad,
imagePull, imagePull,
imagePush, imagePush,
@ -186,6 +222,15 @@ const paginationConfig = reactive({
}); });
type FormInstance = InstanceType<typeof ElForm>; type FormInstance = InstanceType<typeof ElForm>;
const buildVisiable = ref(false);
const buildFormRef = ref<FormInstance>();
const buildForm = reactive({
from: 'path',
dockerfile: '',
tag: '',
});
const pullVisiable = ref(false); const pullVisiable = ref(false);
const pullFormRef = ref<FormInstance>(); const pullFormRef = ref<FormInstance>();
const pullForm = reactive({ const pullForm = reactive({
@ -233,6 +278,9 @@ const loadRepos = async () => {
repos.value = res.data; repos.value = res.data;
}; };
const loadBuildDir = async (path: string) => {
buildForm.dockerfile = path;
};
const loadSaveDir = async (path: string) => { const loadSaveDir = async (path: string) => {
saveForm.path = path; saveForm.path = path;
}; };
@ -284,6 +332,29 @@ const submitPush = async (formEl: FormInstance | undefined) => {
}); });
}; };
const onOpenBuild = () => {
buildVisiable.value = true;
buildForm.from = 'path';
buildForm.dockerfile = '';
};
const submitBuild = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
try {
loading.value = true;
loadVisiable.value = false;
await imageBuild(buildForm);
loading.value = false;
search();
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
} catch {
loading.value = false;
search();
}
});
};
const onOpenload = () => { const onOpenload = () => {
loadVisiable.value = true; loadVisiable.value = true;
loadForm.path = ''; loadForm.path = '';

View File

@ -25,6 +25,7 @@
<Container v-if="activeNames === 'container'" /> <Container v-if="activeNames === 'container'" />
<Repo v-if="activeNames === 'repo'" /> <Repo v-if="activeNames === 'repo'" />
<Image v-if="activeNames === 'image'" /> <Image v-if="activeNames === 'image'" />
<Network v-if="activeNames === 'network'" />
<Monitor v-if="activeNames === 'storage'" /> <Monitor v-if="activeNames === 'storage'" />
<About v-if="activeNames === 'schedule'" /> <About v-if="activeNames === 'schedule'" />
</div> </div>
@ -35,6 +36,7 @@ import { ref } from 'vue';
import Container from '@/views/container/container/index.vue'; import Container from '@/views/container/container/index.vue';
import Repo from '@/views/container/repo/index.vue'; import Repo from '@/views/container/repo/index.vue';
import Image from '@/views/container/image/index.vue'; import Image from '@/views/container/image/index.vue';
import Network from '@/views/container/network/index.vue';
import Monitor from '@/views/setting/tabs/monitor.vue'; import Monitor from '@/views/setting/tabs/monitor.vue';
import About from '@/views/setting/tabs/about.vue'; import About from '@/views/setting/tabs/about.vue';

View File

@ -0,0 +1,110 @@
<template>
<el-dialog v-model="createVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
<template #header>
<div class="card-header">
<span>{{ $t('container.createNetwork') }}</span>
</div>
</template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item :label="$t('container.networkName')" prop="name">
<el-input clearable v-model="form.name" />
</el-form-item>
<el-form-item :label="$t('container.driver')" prop="driver">
<el-select v-model="form.driver">
<el-option label="bridge" value="bridge" />
<el-option label="ipvlan" value="ipvlan" />
<el-option label="macvlan" value="macvlan" />
<el-option label="overlay" value="overlay" />
</el-select>
</el-form-item>
<el-form-item :label="$t('container.option')" prop="optionStr">
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" v-model="form.optionStr" />
</el-form-item>
<el-form-item :label="$t('container.subnet')" prop="ipv4Subnet">
<el-input clearable v-model="form.ipv4Subnet" />
</el-form-item>
<el-form-item :label="$t('container.gateway')" prop="ipv4Gateway">
<el-input clearable v-model="form.ipv4Gateway" />
</el-form-item>
<el-form-item :label="$t('container.scope')" prop="scope">
<el-input clearable v-model="form.scope" />
</el-form-item>
<el-form-item :label="$t('container.tag')" prop="labelStr">
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" v-model="form.labelStr" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="createVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm, ElMessage } from 'element-plus';
import { createNetwork } from '@/api/modules/container';
const createVisiable = ref(false);
const form = reactive({
name: '',
labelStr: '',
labels: [] as Array<string>,
optionStr: '',
options: [] as Array<string>,
driver: '',
ipv4Subnet: '',
ipv4Gateway: '',
scope: '',
});
const acceptParams = (): void => {
createVisiable.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
const rules = reactive({
name: [Rules.requiredInput, Rules.name],
driver: [Rules.requiredSelect],
ipv4Subnet: [Rules.requiredInput],
ipv4Gateway: [Rules.requiredInput],
scope: [Rules.requiredInput],
});
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
function restForm() {
if (formRef.value) {
formRef.value.resetFields();
}
}
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
if (form.labelStr !== '') {
form.labels = form.labelStr.split('\n');
}
if (form.optionStr !== '') {
form.options = form.optionStr.split('\n');
}
await createNetwork(form);
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
restForm();
emit('search');
createVisiable.value = false;
});
};
defineExpose({
acceptParams,
});
</script>

View File

@ -0,0 +1,140 @@
<template>
<div>
<el-card style="margin-top: 20px">
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
<template #toolbar>
<el-button style="margin-left: 10px" @click="onCreate()">
{{ $t('commons.button.create') }}
</el-button>
<el-button type="danger" plain :disabled="selects.length === 0" @click="batchDelete(null)">
{{ $t('commons.button.delete') }}
</el-button>
</template>
<el-table-column type="selection" fix />
<el-table-column
:label="$t('commons.table.name')"
show-overflow-tooltip
min-width="80"
prop="name"
fix
/>
<el-table-column :label="$t('container.driver')" show-overflow-tooltip min-width="40" prop="driver" />
<el-table-column :label="$t('container.attachable')" min-width="40" prop="attachable" fix>
<template #default="{ row }">
<el-icon color="green" v-if="row.attachable"><Select /></el-icon>
<el-icon color="red" v-if="!row.attachable"><CloseBold /></el-icon>
</template>
</el-table-column>
<el-table-column :label="$t('container.subnet')" min-width="80" fix>
<template #default="{ row }">
<div v-if="row.ipv4Subnet.length !== 0">
ipv4
<el-tag>{{ row.ipv4Subnet }}</el-tag>
</div>
<div v-if="row.ipv6Subnet.length !== 0">
ipv6
<el-tag>{{ row.ipv6Subnet }}</el-tag>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('container.gateway')" min-width="80" fix>
<template #default="{ row }">
<div v-if="row.ipv4Gateway.length !== 0">
ipv4
<el-tag>{{ row.ipv4Gateway }}</el-tag>
</div>
<div v-if="row.ipv6Gateway.length !== 0">
ipv6
<el-tag>{{ row.ipv6Gateway }}</el-tag>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('container.tag')" min-width="140" fix>
<template #default="{ row }">
<div v-for="(item, index) of row.labels" :key="index">
<el-tooltip class="item" :content="item" placement="top">
<el-tag>{{ item }}</el-tag>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column
prop="createdAt"
min-width="90"
:label="$t('commons.table.date')"
:formatter="dateFromat"
/>
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fix />
</ComplexTable>
</el-card>
<CreateDialog @search="search" ref="dialogCreateRef" />
</div>
</template>
<script lang="ts" setup>
import ComplexTable from '@/components/complex-table/index.vue';
import CreateDialog from '@/views/container/network/create/index.vue';
import { reactive, onMounted, ref } from 'vue';
import { dateFromat } from '@/utils/util';
import { deleteNetwork, getNetworkPage } from '@/api/modules/container';
import { Container } from '@/api/interface/container';
import i18n from '@/lang';
import { useDeleteData } from '@/hooks/use-delete-data';
const data = ref();
const selects = ref<any>([]);
const paginationConfig = reactive({
page: 1,
pageSize: 10,
total: 0,
});
const dialogCreateRef = ref<DialogExpose>();
interface DialogExpose {
acceptParams: () => void;
}
const onCreate = async () => {
dialogCreateRef.value!.acceptParams();
};
const search = async () => {
const params = {
page: paginationConfig.page,
pageSize: paginationConfig.pageSize,
};
await getNetworkPage(params).then((res) => {
if (res.data) {
data.value = res.data.items;
}
paginationConfig.total = res.data.total;
});
};
const batchDelete = async (row: Container.NetworkInfo | null) => {
let ids: Array<string> = [];
if (row === null) {
selects.value.forEach((item: Container.NetworkInfo) => {
ids.push(item.id);
});
} else {
ids.push(row.id);
}
await useDeleteData(deleteNetwork, { ids: ids }, 'commons.msg.delete', true);
search();
};
const buttons = [
{
label: i18n.global.t('commons.button.delete'),
click: (row: Container.NetworkInfo) => {
batchDelete(row);
},
},
];
onMounted(() => {
search();
});
</script>

19
go.mod
View File

@ -22,7 +22,7 @@ require (
github.com/gwatts/gin-adapter v1.0.0 github.com/gwatts/gin-adapter v1.0.0
github.com/jinzhu/copier v0.3.5 github.com/jinzhu/copier v0.3.5
github.com/joho/godotenv v1.4.0 github.com/joho/godotenv v1.4.0
github.com/kr/pty v1.1.1 github.com/kr/pty v1.1.5
github.com/mholt/archiver/v4 v4.0.0-alpha.7 github.com/mholt/archiver/v4 v4.0.0-alpha.7
github.com/minio/minio-go/v7 v7.0.36 github.com/minio/minio-go/v7 v7.0.36
github.com/mojocn/base64Captcha v1.3.5 github.com/mojocn/base64Captcha v1.3.5
@ -54,11 +54,15 @@ require (
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/Microsoft/hcsshim v0.9.4 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect github.com/andybalholm/brotli v1.0.4 // indirect
github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/containerd/cgroups v1.0.3 // indirect
github.com/containerd/containerd v1.6.8 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/distribution/distribution/v3 v3.0.0-20220725133111-4bf3547399eb // indirect github.com/distribution/distribution/v3 v3.0.0-20220725133111-4bf3547399eb // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/distribution v2.8.1+incompatible // indirect
@ -107,13 +111,15 @@ require (
github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/sys/mount v0.3.3 // indirect
github.com/moby/sys/mountinfo v0.6.2 // indirect
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
github.com/opencontainers/runc v1.1.4 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.2 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect github.com/pierrec/lz4/v4 v4.1.15 // indirect
@ -132,16 +138,17 @@ require (
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.opencensus.io v0.23.0 // indirect go.opencensus.io v0.23.0 // indirect
go.opentelemetry.io/otel v1.0.0 // indirect go.opentelemetry.io/otel v1.3.0 // indirect
go.opentelemetry.io/otel/trace v1.0.0 // indirect go.opentelemetry.io/otel/trace v1.3.0 // indirect
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/tools v0.1.12 // indirect golang.org/x/tools v0.1.12 // indirect
google.golang.org/protobuf v1.28.0 // indirect google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/ini.v1 v1.66.6 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.3.0 // indirect gotest.tools/v3 v3.3.0 // indirect
) )

654
go.sum

File diff suppressed because it is too large Load Diff