mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 00:09:16 +08:00
feat: 容器创建功能实现
This commit is contained in:
parent
28df0b9a3c
commit
53845e60b6
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -10,3 +10,7 @@ type Response struct {
|
||||
Msg string `json:"msg"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Option string `json:"option"`
|
||||
}
|
||||
|
@ -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"`
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
113
backend/app/service/image_test.go
Normal file
113
backend/app/service/image_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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-failure(five 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',
|
||||
|
@ -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: '镜像名称及 Tag,例:nginx:latest',
|
||||
|
||||
network: '网络',
|
||||
createNetwork: '添加网络',
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -156,7 +156,7 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<CreateDialog ref="dialogCreateRef" />
|
||||
<CreateDialog @search="search" ref="dialogCreateRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
4
go.mod
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user