1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-19 00:09:16 +08:00

feat: 容器创建功能实现

This commit is contained in:
ssongliu 2022-10-12 18:55:47 +08:00 committed by ssongliu
parent 28df0b9a3c
commit 53845e60b6
18 changed files with 617 additions and 126 deletions

View File

@ -30,6 +30,23 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
})
}
func (b *BaseApi) ContainerCreate(c *gin.Context) {
var req dto.ContainerCreate
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.ContainerCreate(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) ContainerOperation(c *gin.Context) {
var req dto.ContainerOperation
if err := c.ShouldBindJSON(&req); err != nil {
@ -161,6 +178,14 @@ func (b *BaseApi) SearchVolume(c *gin.Context) {
Total: total,
})
}
func (b *BaseApi) ListVolume(c *gin.Context) {
list, err := containerService.ListVolume()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, list)
}
func (b *BaseApi) DeleteVolume(c *gin.Context) {
var req dto.BatchDelete
if err := c.ShouldBindJSON(&req); err != nil {

View File

@ -31,6 +31,15 @@ func (b *BaseApi) SearchImage(c *gin.Context) {
})
}
func (b *BaseApi) ListImage(c *gin.Context) {
list, err := imageService.List()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, list)
}
func (b *BaseApi) ImageBuild(c *gin.Context) {
var req dto.ImageBuild
if err := c.ShouldBindJSON(&req); err != nil {

View File

@ -10,3 +10,7 @@ type Response struct {
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
type Options struct {
Option string `json:"option"`
}

View File

@ -22,6 +22,31 @@ type ContainerInfo struct {
RunTime string `json:"runTime"`
}
type ContainerCreate struct {
Name string `json:"name"`
Image string `json:"image"`
PublishAllPorts bool `json:"publishAllPorts"`
ExposedPorts []PortHelper `json:"exposedPorts"`
Cmd []string `json:"cmd"`
NanoCPUs int64 `json:"nanoCPUs"`
Memory int64 `json:"memory"`
AutoRemove bool `json:"autoRemove"`
Volumes []VolumeHelper `json:"volumes"`
Labels []string `json:"labels"`
Env []string `json:"env"`
RestartPolicy string `json:"restartPolicy"`
}
type VolumeHelper struct {
SourceDir string `json:"sourceDir"`
ContainerDir string `json:"containerDir"`
Mode string `json:"mode"`
}
type PortHelper struct {
ContainerPort int `json:"containerPort"`
HostPort int `json:"hostPort"`
}
type ContainerLog struct {
ContainerID string `json:"containerID" validate:"required"`
Mode string `json:"mode" validate:"required"`

View File

@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
@ -13,10 +14,13 @@ import (
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/utils/docker"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"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/go-connections/nat"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
type ContainerService struct{}
@ -25,6 +29,8 @@ type IContainerService interface {
Page(req dto.PageContainer) (int64, interface{}, error)
PageNetwork(req dto.PageInfo) (int64, interface{}, error)
PageVolume(req dto.PageInfo) (int64, interface{}, error)
ListVolume() ([]dto.Options, error)
ContainerCreate(req dto.ContainerCreate) error
ContainerOperation(req dto.ContainerOperation) error
ContainerLogs(param dto.ContainerLog) (string, error)
Inspect(req dto.InspectReq) (string, error)
@ -101,6 +107,55 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
return string(bytes), nil
}
func (u *ContainerService) ContainerCreate(req dto.ContainerCreate) error {
client, err := docker.NewDockerClient()
if err != nil {
return err
}
config := &container.Config{
Image: req.Image,
Cmd: req.Cmd,
Env: req.Env,
Labels: stringsToMap(req.Labels),
}
hostConf := &container.HostConfig{
AutoRemove: req.AutoRemove,
PublishAllPorts: req.PublishAllPorts,
RestartPolicy: container.RestartPolicy{Name: req.RestartPolicy},
}
if req.RestartPolicy == "on-failure" {
hostConf.RestartPolicy.MaximumRetryCount = 5
}
if req.NanoCPUs != 0 {
hostConf.NanoCPUs = req.NanoCPUs * 1000000000
}
if req.Memory != 0 {
hostConf.Memory = req.Memory
}
if len(req.ExposedPorts) != 0 {
hostConf.PortBindings = make(nat.PortMap)
for _, port := range req.ExposedPorts {
bindItem := nat.PortBinding{HostPort: strconv.Itoa(port.HostPort)}
hostConf.PortBindings[nat.Port(fmt.Sprintf("%d/tcp", port.ContainerPort))] = []nat.PortBinding{bindItem}
}
}
if len(req.Volumes) != 0 {
config.Volumes = make(map[string]struct{})
for _, volume := range req.Volumes {
config.Volumes[volume.ContainerDir] = struct{}{}
hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", volume.SourceDir, volume.ContainerDir, volume.Mode))
}
}
container, err := client.ContainerCreate(context.TODO(), config, hostConf, &network.NetworkingConfig{}, &v1.Platform{}, req.Name)
if err != nil {
return err
}
if err := client.ContainerStart(context.TODO(), container.ID, types.ContainerStartOptions{}); err != nil {
return fmt.Errorf("create successful but start failed, err: %v", err)
}
return nil
}
func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error {
var err error
ctx := context.Background()
@ -293,6 +348,23 @@ func (u *ContainerService) PageVolume(req dto.PageInfo) (int64, interface{}, err
return int64(total), data, nil
}
func (u *ContainerService) ListVolume() ([]dto.Options, error) {
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
list, err := client.VolumeList(context.TODO(), filters.NewArgs())
if err != nil {
return nil, err
}
var data []dto.Options
for _, item := range list.Volumes {
data = append(data, dto.Options{
Option: item.Name,
})
}
return data, nil
}
func (u *ContainerService) DeleteVolume(req dto.BatchDelete) error {
client, err := docker.NewDockerClient()
if err != nil {

View File

@ -23,6 +23,7 @@ type ImageService struct{}
type IImageService interface {
Page(req dto.PageInfo) (int64, interface{}, error)
List() ([]dto.Options, error)
ImagePull(req dto.ImagePull) error
ImageLoad(req dto.ImageLoad) error
ImageSave(req dto.ImageSave) error
@ -43,7 +44,7 @@ func (u *ImageService) Page(req dto.PageInfo) (int64, interface{}, error) {
if err != nil {
return 0, nil, err
}
list, err = client.ImageList(context.Background(), types.ImageListOptions{All: true})
list, err = client.ImageList(context.Background(), types.ImageListOptions{})
if err != nil {
return 0, nil, err
}
@ -70,6 +71,29 @@ func (u *ImageService) Page(req dto.PageInfo) (int64, interface{}, error) {
return int64(total), backDatas, nil
}
func (u *ImageService) List() ([]dto.Options, error) {
var (
list []types.ImageSummary
backDatas []dto.Options
)
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
list, err = client.ImageList(context.Background(), types.ImageListOptions{})
if err != nil {
return nil, err
}
for _, image := range list {
for _, tag := range image.RepoTags {
backDatas = append(backDatas, dto.Options{
Option: tag,
})
}
}
return backDatas, nil
}
func (u *ImageService) ImageBuild(req dto.ImageBuild) (string, error) {
client, err := docker.NewDockerClient()
if err != nil {

View File

@ -0,0 +1,113 @@
package service
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"testing"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/utils/docker"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/pkg/archive"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
func TestImage(t *testing.T) {
file, err := os.OpenFile(("/tmp/nginx.tar"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
fmt.Println(err)
}
defer file.Close()
client, err := docker.NewDockerClient()
if err != nil {
fmt.Println(err)
}
out, err := client.ImageSave(context.TODO(), []string{"nginx:1.14.2"})
fmt.Println(err)
defer out.Close()
if _, err = io.Copy(file, out); err != nil {
fmt.Println(err)
}
}
func TestBuild(t *testing.T) {
client, err := docker.NewDockerClient()
if err != nil {
fmt.Println(err)
}
tar, err := archive.TarWithOptions("/tmp/testbuild/", &archive.TarOptions{})
if err != nil {
fmt.Println(err)
}
opts := types.ImageBuildOptions{
Dockerfile: "Dockerfile",
Tags: []string{"hello/test:v1"},
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) {
file, err := ioutil.ReadFile(constant.DaemonJsonDir)
if err != nil {
fmt.Println(err)
}
deamonMap := make(map[string]interface{})
err = json.Unmarshal(file, &deamonMap)
fmt.Println(err)
for k, v := range deamonMap {
fmt.Println(k, v)
}
if _, ok := deamonMap["insecure-registries"]; ok {
if k, v := deamonMap["insecure-registries"].(string); v {
fmt.Println("string ", k)
}
if k, v := deamonMap["insecure-registries"].([]interface{}); v {
fmt.Println("[]string ", k)
k = append(k, "172.16.10.111:8085")
deamonMap["insecure-registries"] = k
}
}
newss, err := json.Marshal(deamonMap)
if err != nil {
fmt.Println(err)
}
fmt.Println(string(newss))
if err := ioutil.WriteFile(constant.DaemonJsonDir, newss, 0777); err != nil {
fmt.Println(err)
}
}
func TestNetwork(t *testing.T) {
client, err := docker.NewDockerClient()
if err != nil {
fmt.Println(err)
}
_, err = client.NetworkCreate(context.TODO(), "test", types.NetworkCreate{})
if err != nil {
fmt.Println(err)
}
}
func TestContainer(t *testing.T) {
client, err := docker.NewDockerClient()
if err != nil {
fmt.Println(err)
}
_, err = client.ContainerCreate(context.TODO(), &container.Config{}, &container.HostConfig{}, &network.NetworkingConfig{}, &v1.Platform{}, "test")
if err != nil {
fmt.Println(err)
}
}

View File

@ -22,6 +22,7 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
{
baRouter.POST("/search", baseApi.SearchContainer)
baRouter.POST("/inspect", baseApi.Inspect)
baRouter.POST("", baseApi.ContainerCreate)
withRecordRouter.POST("operate", baseApi.ContainerOperation)
withRecordRouter.POST("/log", baseApi.ContainerLogs)
@ -32,6 +33,7 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
withRecordRouter.POST("/repo/del", baseApi.DeleteRepo)
baRouter.POST("/image/search", baseApi.SearchImage)
baRouter.GET("/image", baseApi.ListImage)
baRouter.POST("/image/pull", baseApi.ImagePull)
baRouter.POST("/image/push", baseApi.ImagePush)
baRouter.POST("/image/save", baseApi.ImageSave)
@ -45,6 +47,7 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
baRouter.POST("/network", baseApi.CreateNetwork)
baRouter.POST("/volume/del", baseApi.DeleteVolume)
baRouter.POST("/volume/search", baseApi.SearchVolume)
baRouter.GET("/volume", baseApi.ListVolume)
baRouter.POST("/volume", baseApi.CreateVolume)
}
}

View File

@ -4,6 +4,31 @@ export namespace Container {
operation: string;
newName: string;
}
export interface ContainerCreate {
name: string;
image: string;
cmd: Array<string>;
publishAllPorts: boolean;
exposedPorts: Array<Port>;
nanoCPUs: number;
memory: number;
volumes: Array<Volume>;
autoRemove: boolean;
labels: Array<string>;
labelsStr: string;
env: Array<string>;
envStr: string;
restartPolicy: string;
}
export interface Port {
containerPort: number;
hostPort: number;
}
export interface Volume {
sourceDir: string;
containerDir: string;
mode: string;
}
export interface ContainerInfo {
containerID: string;
name: string;
@ -20,6 +45,9 @@ export namespace Container {
id: string;
type: string;
}
export interface Options {
option: string;
}
export interface ImageInfo {
id: string;

View File

@ -5,6 +5,9 @@ import { Container } from '../interface/container';
export const getContainerPage = (params: ReqPage) => {
return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params);
};
export const createContainer = (params: Container.ContainerCreate) => {
return http.post(`/containers`, params);
};
export const getContainerLog = (params: Container.ContainerLogSearch) => {
return http.post<string>(`/containers/log`, params);
@ -22,6 +25,9 @@ export const inspect = (params: Container.ContainerInspect) => {
export const getImagePage = (params: ReqPage) => {
return http.post<ResPage<Container.ImageInfo>>(`/containers/image/search`, params);
};
export const imageOptions = () => {
return http.get<Array<Container.Options>>(`/containers/image`);
};
export const imageBuild = (params: Container.ImageBuild) => {
return http.post<string>(`/containers/image/build`, params);
};
@ -59,6 +65,9 @@ export const createNetwork = (params: Container.NetworkCreate) => {
export const getVolumePage = (params: ReqPage) => {
return http.post<ResPage<Container.VolumeInfo>>(`/containers/volume/search`, params);
};
export const volumeOptions = () => {
return http.get<Array<Container.Options>>(`/containers/volume`);
};
export const deleteVolume = (params: Container.BatchDelete) => {
return http.post(`/containers/volume/del`, params);
};

View File

@ -169,6 +169,30 @@ export default {
lastHour: 'Last Hour',
last10Min: 'Last 10 Minutes',
containerCreate: 'Container create',
port: 'Port',
exposePort: 'Expose port',
exposeAll: 'Expose all',
containerPort: 'Container port',
serverPort: 'Host port',
cmd: 'Command',
cmdHelper: 'one in a row, for example, echo "hello"',
autoRemove: 'Auto remove',
cpuQuota: 'NacosCPU',
memoryLimit: 'Memory',
limitHelper: 'If the limit is 0, the limit is turned off',
mount: 'Mount',
serverPath: 'Server path',
containerDir: 'Container path',
modeRW: 'Read-Write',
modeR: 'Read-Only',
mode: 'Mode',
env: 'Environment',
restartPolicy: 'Restart policy',
unlessStopped: 'unless-stopped',
onFailure: 'on-failurefive times by default',
no: 'no',
image: 'Image',
imagePull: 'Image pull',
imagePush: 'Image push',
@ -179,6 +203,7 @@ export default {
importImage: 'Image import',
import: 'Import',
build: 'Build',
imageBuild: 'Image build',
label: 'Label',
push: 'Push',
fileName: 'FileName',
@ -187,6 +212,9 @@ export default {
version: 'Version',
size: 'Size',
from: 'From',
tag: 'Tag',
tagHelper: 'one in a row, for example, key=value',
imageNameHelper: 'Image name and Tag, for example: nginx:latest',
network: 'Network',
createNetwork: 'Create network',

View File

@ -166,6 +166,30 @@ export default {
lastHour: '最近 1 小时',
last10Min: '最近 10 分钟',
containerCreate: '容器创建',
port: '端口',
exposePort: '暴露端口',
exposeAll: '暴露所有',
containerPort: '容器端口',
serverPort: '服务器端口',
cmd: '启动命令',
cmdHelper: '一行一个 echo "hello"',
autoRemove: '容器退出后自动删除容器',
cpuQuota: 'CPU 限制',
memoryLimit: '内存限制',
limitHelper: '限制为 0 则关闭限制',
mount: '挂载卷',
serverPath: '服务器目录',
containerDir: '容器目录',
modeRW: '读写',
modeR: '只读',
mode: '权限',
env: '环境变量',
restartPolicy: '重启规则',
unlessStopped: '关闭后重启',
onFailure: '失败后重启默认重启 5 ',
no: '不重启',
image: '镜像',
imagePull: '拉取镜像',
imagePush: '推送镜像',
@ -176,6 +200,7 @@ export default {
path: '路径',
importImage: '导入镜像',
import: '导入',
imageBuild: '构建镜像',
build: '构建镜像',
edit: '编辑',
pathSelect: '路径选择',
@ -188,6 +213,8 @@ export default {
size: '大小',
from: '来源',
tag: '标签',
tagHelper: '一行一个 key=value',
imageNameHelper: '镜像名称及 Tagnginx:latest',
network: '网络',
createNetwork: '添加网络',

View File

@ -2,93 +2,174 @@
<el-dialog v-model="createVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="50%">
<template #header>
<div class="card-header">
<span>容器创建</span>
<span>{{ $t('container.containerCreate') }}</span>
</div>
</template>
<el-form ref="formRef" :model="form" label-position="left" :rules="rules" label-width="120px">
<el-form-item label="容器名称" prop="name">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item :label="$t('container.name')" prop="name">
<el-input clearable v-model="form.name" />
</el-form-item>
<el-form-item label="镜像" prop="image">
<el-input clearable v-model="form.image" />
<el-form-item :label="$t('container.image')" prop="image">
<el-select style="width: 100%" filterable v-model="form.image">
<el-option v-for="(item, index) of images" :key="index" :value="item.option" :label="item.option" />
</el-select>
</el-form-item>
<el-form-item label="端口" prop="image">
<el-form-item :label="$t('container.port')">
<el-radio-group v-model="form.publishAllPorts" class="ml-4">
<el-radio :label="false">暴露端口</el-radio>
<el-radio :label="true">暴露所有</el-radio>
<el-radio :label="false">{{ $t('container.exposePort') }}</el-radio>
<el-radio :label="true">{{ $t('container.exposeAll') }}</el-radio>
</el-radio-group>
<div style="margin-top: 20px"></div>
<table style="width: 100%; margin-top: 5px" class="tab-table">
<tr v-for="(row, index) in ports" :key="index">
<td width="48%">
<el-input v-model="row['key']" />
</td>
<td width="48%">
<el-input v-model="row['value']" />
</td>
<td>
<el-button type="text" style="font-size: 10px" @click="handlePortsDelete(index)">
{{ $t('commons.button.delete') }}
</el-button>
</td>
</tr>
<tr>
<td align="left">
<el-button @click="handlePortsAdd()">{{ $t('commons.button.add') }}</el-button>
</td>
</tr>
</table>
</el-form-item>
<el-form-item label="启动命令" prop="command">
<el-input clearable v-model="form.command" />
<el-form-item v-if="!form.publishAllPorts">
<el-card style="width: 100%">
<table style="width: 100%" class="tab-table">
<tr v-if="form.exposedPorts.length !== 0">
<th scope="col" width="48%" align="left">
<label>{{ $t('container.containerPort') }}</label>
</th>
<th scope="col" width="48%" align="left">
<label>{{ $t('container.serverPort') }}</label>
</th>
<th align="left"></th>
</tr>
<tr v-for="(row, index) in form.exposedPorts" :key="index">
<td width="48%">
<el-input-number
:min="0"
:max="65535"
style="width: 100%"
controls-position="right"
v-model.number="row.containerPort"
/>
</td>
<td width="48%">
<el-input-number
:min="0"
:max="65535"
style="width: 100%"
controls-position="right"
v-model.number="row.hostPort"
/>
</td>
<td>
<el-button link style="font-size: 10px" @click="handlePortsDelete(index)">
{{ $t('commons.button.delete') }}
</el-button>
</td>
</tr>
<tr>
<td align="left">
<el-button @click="handlePortsAdd()">{{ $t('commons.button.add') }}</el-button>
</td>
</tr>
</table>
</el-card>
</el-form-item>
<el-form-item :label="$t('container.cmd')" prop="cmdStr">
<el-input
type="textarea"
:placeholder="$t('container.cmdHelper')"
:autosize="{ minRows: 2, maxRows: 4 }"
v-model="form.cmdStr"
/>
</el-form-item>
<el-form-item prop="autoRemove">
<el-checkbox v-model="form.autoRemove">容器停止后自动删除容器</el-checkbox>
<el-checkbox v-model="form.autoRemove">{{ $t('container.autoRemove') }}</el-checkbox>
</el-form-item>
<el-form-item label="限制CPU" prop="cpusetCpus">
<el-input v-model="form.cpusetCpus" />
<el-form-item :label="$t('container.cpuQuota')" prop="nanoCPUs">
<el-input type="number" style="width: 40%" v-model.number="form.nanoCPUs">
<template #append><div style="width: 60px">Core</div></template>
</el-input>
<span class="input-help">{{ $t('container.limitHelper') }}</span>
</el-form-item>
<el-form-item label="内存" prop="memeryLimit">
<el-input v-model="form.memeryLimit" />
<el-form-item :label="$t('container.memoryLimit')" prop="memoryItem">
<el-input style="width: 40%" v-model.number="form.memoryItem">
<template #append>
<el-select v-model="form.memoryUnit" placeholder="Select" style="width: 100px">
<el-option label="KB" value="KB" />
<el-option label="MB" value="MB" />
<el-option label="GB" value="GB" />
</el-select>
</template>
</el-input>
<span class="input-help">{{ $t('container.limitHelper') }}</span>
</el-form-item>
<el-form-item label="挂载卷">
<div style="margin-top: 20px"></div>
<table style="width: 100%; margin-top: 5px" class="tab-table">
<tr v-for="(row, index) in volumes" :key="index">
<td width="30%">
<el-input v-model="row['name']" />
</td>
<td width="30%">
<el-input v-model="row['bind']" />
</td>
<td width="30%">
<el-input v-model="row['mode']" />
</td>
<td>
<el-button type="text" style="font-size: 10px" @click="handleVolumesDelete(index)">
{{ $t('commons.button.delete') }}
</el-button>
</td>
</tr>
<tr>
<td align="left">
<el-button @click="handleVolumesAdd()">{{ $t('commons.button.add') }}</el-button>
</td>
</tr>
</table>
<el-form-item :label="$t('container.mount')">
<el-card style="width: 100%">
<table style="width: 100%" class="tab-table">
<tr v-if="form.volumes.length !== 0">
<th scope="col" width="32%" align="left">
<label>{{ $t('container.serverPath') }}</label>
</th>
<th scope="col" width="32%" align="left">
<label>{{ $t('container.mode') }}</label>
</th>
<th scope="col" width="32%" align="left">
<label>{{ $t('container.containerDir') }}</label>
</th>
<th align="left"></th>
</tr>
<tr v-for="(row, index) in form.volumes" :key="index">
<td width="32%">
<el-select
style="width: 100%"
allow-create
clearable
filterable
v-model="row.sourceDir"
>
<el-option
v-for="(item, indexV) of volumes"
:key="indexV"
:value="item.option"
:label="item.option"
/>
</el-select>
</td>
<td width="32%">
<el-select style="width: 100%" filterable v-model="row.mode">
<el-option value="rw" :label="$t('container.modeRW')" />
<el-option value="ro" :label="$t('container.modeR')" />
</el-select>
</td>
<td width="32%">
<el-input v-model="row.containerDir" />
</td>
<td>
<el-button link style="font-size: 10px" @click="handleVolumesDelete(index)">
{{ $t('commons.button.delete') }}
</el-button>
</td>
</tr>
<tr>
<td align="left">
<el-button @click="handleVolumesAdd()">{{ $t('commons.button.add') }}</el-button>
</td>
</tr>
</table>
</el-card>
</el-form-item>
<el-form-item label="标签" prop="labels">
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" v-model="form.labels" />
<el-form-item :label="$t('container.tag')" prop="labelsStr">
<el-input
type="textarea"
:placeholder="$t('container.tagHelper')"
:autosize="{ minRows: 2, maxRows: 4 }"
v-model="form.labelsStr"
/>
</el-form-item>
<el-form-item label="环境变量(每行一个)" prop="environment">
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" v-model="form.environment" />
<el-form-item :label="$t('container.env')" prop="envStr">
<el-input
type="textarea"
:placeholder="$t('container.tagHelper')"
:autosize="{ minRows: 2, maxRows: 4 }"
v-model="form.envStr"
/>
</el-form-item>
<el-form-item label="重启规则" prop="restartPolicy.value">
<el-radio-group v-model="form.restartPolicy.value">
<el-radio :label="false">关闭后马上重启</el-radio>
<el-radio :label="false">错误时重启默认重启 5 </el-radio>
<el-radio :label="true">不重启</el-radio>
<el-form-item :label="$t('container.restartPolicy')" prop="restartPolicy">
<el-radio-group v-model="form.restartPolicy">
<el-radio label="unless-stopped">{{ $t('container.unlessStopped') }}</el-radio>
<el-radio label="on-failure">{{ $t('container.onFailure') }}</el-radio>
<el-radio label="no">{{ $t('container.no') }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
@ -108,51 +189,47 @@ import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElForm, ElMessage } from 'element-plus';
import { imageOptions, volumeOptions, createContainer } from '@/api/modules/container';
import { Container } from '@/api/interface/container';
const createVisiable = ref(false);
const form = reactive({
name: '',
image: '',
command: '',
cmdStr: '',
cmd: [] as Array<string>,
publishAllPorts: false,
ports: [],
cpusetCpus: 1,
memeryLimit: 100,
volumes: [],
exposedPorts: [] as Array<Container.Port>,
nanoCPUs: 1,
memory: 100,
memoryItem: 100,
memoryUnit: 'MB',
volumes: [] as Array<Container.Volume>,
autoRemove: false,
labels: '',
environment: '',
restartPolicy: {
value: '',
name: '',
maximumRetryCount: '',
},
labels: [] as Array<string>,
labelsStr: '',
env: [] as Array<string>,
envStr: '',
restartPolicy: '',
});
const ports = ref();
const images = ref();
const volumes = ref();
const acceptParams = (): void => {
createVisiable.value = true;
form.restartPolicy = 'no';
form.memoryUnit = 'MB';
loadImageOptions();
loadVolumeOptions();
};
const emit = defineEmits<{ (e: 'search'): void }>();
const rules = reactive({
name: [Rules.requiredInput, Rules.name],
type: [Rules.requiredSelect],
specType: [Rules.requiredSelect],
week: [Rules.requiredSelect, Rules.number],
day: [Rules.number, { max: 31, min: 1 }],
hour: [Rules.number, { max: 23, min: 0 }],
minute: [Rules.number, { max: 60, min: 1 }],
script: [Rules.requiredInput],
website: [Rules.requiredSelect],
database: [Rules.requiredSelect],
url: [Rules.requiredInput],
sourceDir: [Rules.requiredSelect],
targetDirID: [Rules.requiredSelect, Rules.number],
retainCopies: [Rules.number],
image: [Rules.requiredSelect],
nanoCPUs: [Rules.number],
memoryItem: [Rules.number],
});
type FormInstance = InstanceType<typeof ElForm>;
@ -160,39 +237,61 @@ const formRef = ref<FormInstance>();
const handlePortsAdd = () => {
let item = {
key: '',
value: '',
containerPort: 80,
hostPort: 8080,
};
ports.value.push(item);
form.exposedPorts.push(item);
};
const handlePortsDelete = (index: number) => {
ports.value.splice(index, 1);
form.exposedPorts.splice(index, 1);
};
const handleVolumesAdd = () => {
let item = {
from: '',
bind: '',
mode: '',
sourceDir: '',
containerDir: '',
mode: 'rw',
};
volumes.value.push(item);
form.volumes.push(item);
};
const handleVolumesDelete = (index: number) => {
volumes.value.splice(index, 1);
form.volumes.splice(index, 1);
};
function restForm() {
if (formRef.value) {
formRef.value.resetFields();
}
}
const loadImageOptions = async () => {
const res = await imageOptions();
images.value = res.data;
};
const loadVolumeOptions = async () => {
const res = await volumeOptions();
volumes.value = res.data;
};
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
if (form.envStr.length !== 0) {
form.env = form.envStr.split('\n');
}
if (form.labelsStr.length !== 0) {
form.labels = form.labelsStr.split('\n');
}
if (form.cmdStr.length !== 0) {
form.cmd = form.cmdStr.split('\n');
}
switch (form.memoryUnit) {
case 'KB':
form.memory = form.memoryItem * 1024;
break;
case 'MB':
form.memory = form.memoryItem * 1024 * 1024;
break;
case 'GB':
form.memory = form.memoryItem * 1024 * 1024 * 1024;
break;
}
await createContainer(form);
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
restForm();
emit('search');
createVisiable.value = false;
});

View File

@ -156,7 +156,7 @@
</span>
</template>
</el-dialog>
<CreateDialog ref="dialogCreateRef" />
<CreateDialog @search="search" ref="dialogCreateRef" />
</div>
</template>

View File

@ -8,12 +8,12 @@
>
<template #header>
<div class="card-header">
<span>{{ $t('container.importImage') }}</span>
<span>{{ $t('container.buildImage') }}</span>
</div>
</template>
<el-form ref="formRef" :model="form" label-width="80px">
<el-form-item :label="$t('container.name')" :rules="Rules.requiredInput" prop="name">
<el-input v-model="form.name" clearable />
<el-input :placeholder="$t('container.imageNameHelper')" v-model="form.name" clearable />
</el-form-item>
<el-form-item label="Dockerfile" :rules="Rules.requiredSelect" prop="from">
<el-radio-group v-model="form.from">
@ -32,7 +32,12 @@
</el-input>
</el-form-item>
<el-form-item :label="$t('container.tag')">
<el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" v-model="form.tagStr" />
<el-input
:placeholder="$t('container.tagHelper')"
type="textarea"
:autosize="{ minRows: 2, maxRows: 4 }"
v-model="form.tagStr"
/>
</el-form-item>
</el-form>
@ -55,7 +60,7 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="onSubmit(formRef)">{{ $t('container.import') }}</el-button>
<el-button @click="onSubmit(formRef)">{{ $t('container.build') }}</el-button>
<el-button @click="buildVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
</span>
</template>

View File

@ -18,7 +18,12 @@
</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-input
type="textarea"
:placeholder="$t('container.tagHelper')"
:autosize="{ minRows: 2, maxRows: 4 }"
v-model="form.optionStr"
/>
</el-form-item>
<el-form-item :label="$t('container.subnet')" prop="subnet">
<el-input clearable v-model="form.subnet" />
@ -30,7 +35,12 @@
<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-input
type="textarea"
:placeholder="$t('container.tagHelper')"
:autosize="{ minRows: 2, maxRows: 4 }"
v-model="form.labelStr"
/>
</el-form-item>
</el-form>
<template #footer>

View File

@ -15,10 +15,20 @@
</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-input
type="textarea"
:placeholder="$t('container.tagHelper')"
:autosize="{ minRows: 2, maxRows: 4 }"
v-model="form.optionStr"
/>
</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-input
type="textarea"
:placeholder="$t('container.tagHelper')"
:autosize="{ minRows: 2, maxRows: 4 }"
v-model="form.labelStr"
/>
</el-form-item>
</el-form>
<template #footer>

4
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/compose-spec/compose-go v1.6.0
github.com/dgraph-io/badger/v3 v3.2103.2
github.com/docker/docker v20.10.18+incompatible
github.com/docker/go-connections v0.4.0
github.com/fsnotify/fsnotify v1.5.4
github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6
github.com/gabriel-vasile/mimetype v1.4.1
@ -28,6 +29,7 @@ require (
github.com/mojocn/base64Captcha v1.3.5
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/nicksnyder/go-i18n/v2 v2.1.2
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.1
github.com/robfig/cron/v3 v3.0.1
@ -66,7 +68,6 @@ require (
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/distribution/distribution/v3 v3.0.0-20220725133111-4bf3547399eb // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
@ -118,7 +119,6 @@ require (
github.com/modern-go/reflect2 v1.0.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/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/v2 v2.0.2 // indirect