feat: 完成镜像拉取、推送、导入、导出功能
This commit is contained in:
@@ -15,6 +15,7 @@ var (
|
|||||||
groupService = service.ServiceGroupApp.GroupService
|
groupService = service.ServiceGroupApp.GroupService
|
||||||
containerService = service.ServiceGroupApp.ContainerService
|
containerService = service.ServiceGroupApp.ContainerService
|
||||||
imageRepoService = service.ServiceGroupApp.ImageRepoService
|
imageRepoService = service.ServiceGroupApp.ImageRepoService
|
||||||
|
imageService = service.ServiceGroupApp.ImageService
|
||||||
commandService = service.ServiceGroupApp.CommandService
|
commandService = service.ServiceGroupApp.CommandService
|
||||||
operationService = service.ServiceGroupApp.OperationService
|
operationService = service.ServiceGroupApp.OperationService
|
||||||
fileService = service.ServiceGroupApp.FileService
|
fileService = service.ServiceGroupApp.FileService
|
||||||
|
|||||||
127
backend/app/api/v1/image.go
Normal file
127
backend/app/api/v1/image.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/global"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *BaseApi) SearchImage(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 := imageService.Page(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) ImagePull(c *gin.Context) {
|
||||||
|
var req dto.ImagePull
|
||||||
|
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.ImagePull(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) ImagePush(c *gin.Context) {
|
||||||
|
var req dto.ImagePush
|
||||||
|
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.ImagePush(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) ImageRemove(c *gin.Context) {
|
||||||
|
var req dto.ImageRemove
|
||||||
|
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.ImageRemove(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) ImageSave(c *gin.Context) {
|
||||||
|
var req dto.ImageSave
|
||||||
|
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.ImageSave(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) ImageLoad(c *gin.Context) {
|
||||||
|
var req dto.ImageLoad
|
||||||
|
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.ImageLoad(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
@@ -8,12 +8,16 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b *BaseApi) GetRepoList(c *gin.Context) {
|
func (b *BaseApi) SearchRepo(c *gin.Context) {
|
||||||
var req dto.PageInfo
|
var req dto.PageInfo
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := global.VALID.Struct(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
total, list, err := imageRepoService.Page(req)
|
total, list, err := imageRepoService.Page(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -27,6 +31,16 @@ func (b *BaseApi) GetRepoList(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) ListRepo(c *gin.Context) {
|
||||||
|
list, err := imageRepoService.List()
|
||||||
|
if err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.SuccessWithData(c, list)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BaseApi) CreateRepo(c *gin.Context) {
|
func (b *BaseApi) CreateRepo(c *gin.Context) {
|
||||||
var req dto.ImageRepoCreate
|
var req dto.ImageRepoCreate
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
|||||||
36
backend/app/dto/image.go
Normal file
36
backend/app/dto/image.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type ImageInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Size string `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageLoad struct {
|
||||||
|
Path string `josn:"path" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageRemove struct {
|
||||||
|
ImageName string `josn:"imageName" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImagePull struct {
|
||||||
|
RepoID uint `josn:"repoID" validate:"required"`
|
||||||
|
ImageName string `josn:"imageName" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImagePush struct {
|
||||||
|
RepoID uint `josn:"repoID" validate:"required"`
|
||||||
|
ImageName string `josn:"imageName" validate:"required"`
|
||||||
|
TagName string `json:"tagName" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageSave struct {
|
||||||
|
ImageName string `josn:"imageName" validate:"required"`
|
||||||
|
Path string `josn:"path" validate:"required"`
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
}
|
||||||
@@ -29,3 +29,9 @@ type ImageRepoInfo struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Auth bool `json:"auth"`
|
Auth bool `json:"auth"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImageRepoOption struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DownloadUrl string `json:"downloadUrl"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type ImageRepoRepo struct{}
|
|||||||
type IImageRepoRepo interface {
|
type IImageRepoRepo interface {
|
||||||
Get(opts ...DBOption) (model.ImageRepo, error)
|
Get(opts ...DBOption) (model.ImageRepo, error)
|
||||||
Page(limit, offset int, opts ...DBOption) (int64, []model.ImageRepo, error)
|
Page(limit, offset int, opts ...DBOption) (int64, []model.ImageRepo, error)
|
||||||
|
List(opts ...DBOption) ([]model.ImageRepo, error)
|
||||||
Create(imageRepo *model.ImageRepo) error
|
Create(imageRepo *model.ImageRepo) error
|
||||||
Update(id uint, vars map[string]interface{}) error
|
Update(id uint, vars map[string]interface{}) error
|
||||||
Delete(opts ...DBOption) error
|
Delete(opts ...DBOption) error
|
||||||
@@ -41,6 +42,18 @@ func (u *ImageRepoRepo) Page(page, size int, opts ...DBOption) (int64, []model.I
|
|||||||
return count, ops, err
|
return count, ops, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ImageRepoRepo) List(opts ...DBOption) ([]model.ImageRepo, error) {
|
||||||
|
var ops []model.ImageRepo
|
||||||
|
db := global.DB.Model(&model.ImageRepo{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
count := int64(0)
|
||||||
|
db = db.Count(&count)
|
||||||
|
err := db.Find(&ops).Error
|
||||||
|
return ops, err
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ImageRepoRepo) Create(imageRepo *model.ImageRepo) error {
|
func (u *ImageRepoRepo) Create(imageRepo *model.ImageRepo) error {
|
||||||
return global.DB.Create(imageRepo).Error
|
return global.DB.Create(imageRepo).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ type ServiceGroup struct {
|
|||||||
HostService
|
HostService
|
||||||
BackupService
|
BackupService
|
||||||
GroupService
|
GroupService
|
||||||
|
ImageService
|
||||||
ImageRepoService
|
ImageRepoService
|
||||||
ContainerService
|
ContainerService
|
||||||
CommandService
|
CommandService
|
||||||
|
|||||||
221
backend/app/service/image.go
Normal file
221
backend/app/service/image.go
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/utils/docker"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImageService struct{}
|
||||||
|
|
||||||
|
type IImageService interface {
|
||||||
|
Page(req dto.PageInfo) (int64, interface{}, error)
|
||||||
|
ImagePull(req dto.ImagePull) error
|
||||||
|
ImageLoad(req dto.ImageLoad) error
|
||||||
|
ImageSave(req dto.ImageSave) error
|
||||||
|
ImagePush(req dto.ImagePush) error
|
||||||
|
ImageRemove(req dto.ImageRemove) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIImageService() IImageService {
|
||||||
|
return &ImageService{}
|
||||||
|
}
|
||||||
|
func (u *ImageService) Page(req dto.PageInfo) (int64, interface{}, error) {
|
||||||
|
var (
|
||||||
|
list []types.ImageSummary
|
||||||
|
records []dto.ImageInfo
|
||||||
|
backDatas []dto.ImageInfo
|
||||||
|
)
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
list, err = client.ImageList(context.Background(), types.ImageListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, image := range list {
|
||||||
|
size := formatFileSize(image.Size)
|
||||||
|
for _, item := range image.RepoTags {
|
||||||
|
name := item[0:strings.LastIndex(item, ":")]
|
||||||
|
tag := strings.ReplaceAll(item[strings.LastIndex(item, ":"):], ":", "")
|
||||||
|
records = append(records, dto.ImageInfo{
|
||||||
|
ID: image.ID,
|
||||||
|
Name: name,
|
||||||
|
Version: tag,
|
||||||
|
CreatedAt: time.Unix(image.Created, 0),
|
||||||
|
Size: size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||||
|
if start > total {
|
||||||
|
backDatas = make([]dto.ImageInfo, 0)
|
||||||
|
} else {
|
||||||
|
if end >= total {
|
||||||
|
end = total
|
||||||
|
}
|
||||||
|
backDatas = records[start:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(total), backDatas, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ImageService) ImagePull(req dto.ImagePull) error {
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.RepoID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
options := types.ImagePullOptions{}
|
||||||
|
if repo.Auth {
|
||||||
|
authConfig := types.AuthConfig{
|
||||||
|
Username: repo.Username,
|
||||||
|
Password: repo.Password,
|
||||||
|
}
|
||||||
|
encodedJSON, err := json.Marshal(authConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
||||||
|
options.RegistryAuth = authStr
|
||||||
|
}
|
||||||
|
image := repo.DownloadUrl + "/" + req.ImageName
|
||||||
|
if len(repo.RepoName) != 0 {
|
||||||
|
image = fmt.Sprintf("%s/%s/%s", repo.DownloadUrl, repo.RepoName, req.ImageName)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
out, err := client.ImagePull(ctx, image, options)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("image %s pull failed, err: %v", image, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
_, _ = buf.ReadFrom(out)
|
||||||
|
global.LOG.Debugf("image %s pull stdout: %v", image, buf.String())
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
|
||||||
|
file, err := os.Open(req.Path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := client.ImageLoad(context.TODO(), file, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ImageService) ImageSave(req dto.ImageSave) error {
|
||||||
|
file, err := os.OpenFile(fmt.Sprintf("%s/%s.tar", req.Path, req.Name), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := client.ImageSave(context.TODO(), []string{req.ImageName})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
if _, err = io.Copy(file, out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ImageService) ImagePush(req dto.ImagePush) error {
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.RepoID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
options := types.ImagePushOptions{}
|
||||||
|
if repo.Auth {
|
||||||
|
authConfig := types.AuthConfig{
|
||||||
|
Username: repo.Username,
|
||||||
|
Password: repo.Password,
|
||||||
|
}
|
||||||
|
encodedJSON, err := json.Marshal(authConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
||||||
|
options.RegistryAuth = authStr
|
||||||
|
}
|
||||||
|
newName := fmt.Sprintf("%s/%s", repo.DownloadUrl, req.TagName)
|
||||||
|
if err := client.ImageTag(context.TODO(), req.ImageName, newName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
out, err := client.ImagePush(context.TODO(), newName, options)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("image %s push failed, err: %v", req.ImageName, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
_, _ = buf.ReadFrom(out)
|
||||||
|
global.LOG.Debugf("image %s push stdout: %v", req.ImageName, buf.String())
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ImageService) ImageRemove(req dto.ImageRemove) error {
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := client.ImageRemove(context.TODO(), req.ImageName, types.ImageRemoveOptions{Force: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatFileSize(fileSize int64) (size string) {
|
||||||
|
if fileSize < 1024 {
|
||||||
|
return fmt.Sprintf("%.2fB", float64(fileSize)/float64(1))
|
||||||
|
} else if fileSize < (1024 * 1024) {
|
||||||
|
return fmt.Sprintf("%.2fKB", float64(fileSize)/float64(1024))
|
||||||
|
} else if fileSize < (1024 * 1024 * 1024) {
|
||||||
|
return fmt.Sprintf("%.2fMB", float64(fileSize)/float64(1024*1024))
|
||||||
|
} else if fileSize < (1024 * 1024 * 1024 * 1024) {
|
||||||
|
return fmt.Sprintf("%.2fGB", float64(fileSize)/float64(1024*1024*1024))
|
||||||
|
} else if fileSize < (1024 * 1024 * 1024 * 1024 * 1024) {
|
||||||
|
return fmt.Sprintf("%.2fTB", float64(fileSize)/float64(1024*1024*1024*1024))
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("%.2fEB", float64(fileSize)/float64(1024*1024*1024*1024*1024))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ type ImageRepoService struct{}
|
|||||||
|
|
||||||
type IImageRepoService interface {
|
type IImageRepoService interface {
|
||||||
Page(search dto.PageInfo) (int64, interface{}, error)
|
Page(search dto.PageInfo) (int64, interface{}, error)
|
||||||
|
List() ([]dto.ImageRepoOption, error)
|
||||||
Create(imageRepoDto dto.ImageRepoCreate) error
|
Create(imageRepoDto dto.ImageRepoCreate) error
|
||||||
Update(id uint, upMap map[string]interface{}) error
|
Update(id uint, upMap map[string]interface{}) error
|
||||||
BatchDelete(ids []uint) error
|
BatchDelete(ids []uint) error
|
||||||
@@ -33,6 +34,19 @@ func (u *ImageRepoService) Page(search dto.PageInfo) (int64, interface{}, error)
|
|||||||
return total, dtoOps, err
|
return total, dtoOps, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ImageRepoService) List() ([]dto.ImageRepoOption, error) {
|
||||||
|
ops, err := imageRepoRepo.List(commonRepo.WithOrderBy("created_at desc"))
|
||||||
|
var dtoOps []dto.ImageRepoOption
|
||||||
|
for _, op := range ops {
|
||||||
|
var item dto.ImageRepoOption
|
||||||
|
if err := copier.Copy(&item, &op); err != nil {
|
||||||
|
return nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||||
|
}
|
||||||
|
dtoOps = append(dtoOps, item)
|
||||||
|
}
|
||||||
|
return dtoOps, err
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ImageRepoService) Create(imageRepoDto dto.ImageRepoCreate) error {
|
func (u *ImageRepoService) Create(imageRepoDto dto.ImageRepoCreate) error {
|
||||||
imageRepo, _ := imageRepoRepo.Get(commonRepo.WithByName(imageRepoDto.RepoName))
|
imageRepo, _ := imageRepoRepo.Get(commonRepo.WithByName(imageRepoDto.RepoName))
|
||||||
if imageRepo.ID != 0 {
|
if imageRepo.ID != 0 {
|
||||||
@@ -48,9 +62,17 @@ func (u *ImageRepoService) Create(imageRepoDto dto.ImageRepoCreate) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ImageRepoService) BatchDelete(ids []uint) error {
|
func (u *ImageRepoService) BatchDelete(ids []uint) error {
|
||||||
|
for _, id := range ids {
|
||||||
|
if id == 1 {
|
||||||
|
return errors.New("The default value cannot be edit !")
|
||||||
|
}
|
||||||
|
}
|
||||||
return imageRepoRepo.Delete(commonRepo.WithIdsIn(ids))
|
return imageRepoRepo.Delete(commonRepo.WithIdsIn(ids))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ImageRepoService) Update(id uint, upMap map[string]interface{}) error {
|
func (u *ImageRepoService) Update(id uint, upMap map[string]interface{}) error {
|
||||||
|
if id == 1 {
|
||||||
|
return errors.New("The default value cannot be deleted !")
|
||||||
|
}
|
||||||
return imageRepoRepo.Update(id, upMap)
|
return imageRepoRepo.Update(id, upMap)
|
||||||
}
|
}
|
||||||
|
|||||||
30
backend/app/service/image_test.go
Normal file
30
backend/app/service/image_test.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/utils/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,9 +25,17 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) {
|
|||||||
withRecordRouter.POST("operate", baseApi.ContainerOperation)
|
withRecordRouter.POST("operate", baseApi.ContainerOperation)
|
||||||
withRecordRouter.POST("/log", baseApi.ContainerLogs)
|
withRecordRouter.POST("/log", baseApi.ContainerLogs)
|
||||||
|
|
||||||
baRouter.POST("/repo/search", baseApi.GetRepoList)
|
baRouter.POST("/repo/search", baseApi.SearchRepo)
|
||||||
baRouter.PUT("/repo/:id", baseApi.UpdateRepo)
|
baRouter.PUT("/repo/:id", baseApi.UpdateRepo)
|
||||||
|
baRouter.GET("/repo", baseApi.ListRepo)
|
||||||
withRecordRouter.POST("/repo", baseApi.CreateRepo)
|
withRecordRouter.POST("/repo", baseApi.CreateRepo)
|
||||||
withRecordRouter.POST("/repo/del", baseApi.DeleteRepo)
|
withRecordRouter.POST("/repo/del", baseApi.DeleteRepo)
|
||||||
|
|
||||||
|
baRouter.POST("/image/search", baseApi.SearchImage)
|
||||||
|
baRouter.POST("/image/pull", baseApi.ImagePull)
|
||||||
|
baRouter.POST("/image/push", baseApi.ImagePush)
|
||||||
|
baRouter.POST("/image/save", baseApi.ImageSave)
|
||||||
|
baRouter.POST("/image/load", baseApi.ImageLoad)
|
||||||
|
baRouter.POST("/image/remove", baseApi.ImageRemove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,34 @@ export namespace Container {
|
|||||||
mode: string;
|
mode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImageInfo {
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
size: string;
|
||||||
|
}
|
||||||
|
export interface ImagePull {
|
||||||
|
repoID: number;
|
||||||
|
imageName: string;
|
||||||
|
}
|
||||||
|
export interface ImagePush {
|
||||||
|
repoID: number;
|
||||||
|
imageName: string;
|
||||||
|
tagName: string;
|
||||||
|
}
|
||||||
|
export interface ImageRemove {
|
||||||
|
imageName: string;
|
||||||
|
}
|
||||||
|
export interface ImageLoad {
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
export interface ImageSave {
|
||||||
|
imageName: string;
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RepoCreate {
|
export interface RepoCreate {
|
||||||
name: string;
|
name: string;
|
||||||
downloadUrl: string;
|
downloadUrl: string;
|
||||||
@@ -47,4 +75,9 @@ export namespace Container {
|
|||||||
password: string;
|
password: string;
|
||||||
auth: boolean;
|
auth: boolean;
|
||||||
}
|
}
|
||||||
|
export interface RepoOptions {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
downloadUrl: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,33 @@ export const getContainerInspect = (containerID: string) => {
|
|||||||
return http.get<string>(`/containers/detail/${containerID}`);
|
return http.get<string>(`/containers/detail/${containerID}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// image
|
||||||
|
export const getImagePage = (params: ReqPage) => {
|
||||||
|
return http.post<ResPage<Container.ImageInfo>>(`/containers/image/search`, params);
|
||||||
|
};
|
||||||
|
export const imagePull = (params: Container.ImagePull) => {
|
||||||
|
return http.post<string>(`/containers/image/pull`, params);
|
||||||
|
};
|
||||||
|
export const imagePush = (params: Container.ImagePush) => {
|
||||||
|
return http.post<string>(`/containers/image/push`, params);
|
||||||
|
};
|
||||||
|
export const imageLoad = (params: Container.ImageLoad) => {
|
||||||
|
return http.post<string>(`/containers/image/load`, params);
|
||||||
|
};
|
||||||
|
export const imageSave = (params: Container.ImageSave) => {
|
||||||
|
return http.post<string>(`/containers/image/save`, params);
|
||||||
|
};
|
||||||
|
export const imageRemove = (params: Container.ImageRemove) => {
|
||||||
|
return http.post(`/containers/image/remove`, 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);
|
||||||
};
|
};
|
||||||
|
export const getRepoOption = () => {
|
||||||
|
return http.get<Container.RepoOptions>(`/containers/repo`);
|
||||||
|
};
|
||||||
export const repoCreate = (params: Container.RepoCreate) => {
|
export const repoCreate = (params: Container.RepoCreate) => {
|
||||||
return http.post(`/containers/repo`, params);
|
return http.post(`/containers/repo`, params);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -161,7 +161,6 @@ export default {
|
|||||||
reName: 'ReName',
|
reName: 'ReName',
|
||||||
remove: 'Remove',
|
remove: 'Remove',
|
||||||
container: 'Container',
|
container: 'Container',
|
||||||
image: 'Image',
|
|
||||||
network: 'Network',
|
network: 'Network',
|
||||||
storage: 'Storage',
|
storage: 'Storage',
|
||||||
schedule: 'Schedule',
|
schedule: 'Schedule',
|
||||||
@@ -172,6 +171,25 @@ export default {
|
|||||||
lastHour: 'Last Hour',
|
lastHour: 'Last Hour',
|
||||||
last10Min: 'Last 10 Minutes',
|
last10Min: 'Last 10 Minutes',
|
||||||
|
|
||||||
|
image: 'Image',
|
||||||
|
pullFromRepo: 'Pull from repo',
|
||||||
|
imagePull: 'Image pull',
|
||||||
|
imagePush: 'Image push',
|
||||||
|
repoName: 'Repo Name',
|
||||||
|
imageName: 'Image name',
|
||||||
|
pull: 'Pull',
|
||||||
|
path: 'Path',
|
||||||
|
importImage: 'Import image',
|
||||||
|
import: 'Import',
|
||||||
|
build: 'Build',
|
||||||
|
label: 'Label',
|
||||||
|
push: 'Push',
|
||||||
|
fileName: 'FileName',
|
||||||
|
export: 'Export',
|
||||||
|
exportImage: 'ExportImage',
|
||||||
|
version: 'Version',
|
||||||
|
size: 'Size',
|
||||||
|
|
||||||
repo: 'Repo',
|
repo: 'Repo',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
downloadUrl: 'Download URL',
|
downloadUrl: 'Download URL',
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ export default {
|
|||||||
reName: '重命名',
|
reName: '重命名',
|
||||||
remove: '移除',
|
remove: '移除',
|
||||||
container: '容器',
|
container: '容器',
|
||||||
image: '镜像',
|
|
||||||
network: '网络',
|
network: '网络',
|
||||||
storage: '数据卷',
|
storage: '数据卷',
|
||||||
schedule: '编排',
|
schedule: '编排',
|
||||||
@@ -169,6 +168,25 @@ export default {
|
|||||||
lastHour: '最近 1 小时',
|
lastHour: '最近 1 小时',
|
||||||
last10Min: '最近 10 分钟',
|
last10Min: '最近 10 分钟',
|
||||||
|
|
||||||
|
image: '镜像',
|
||||||
|
pullFromRepo: '从仓库中拉取',
|
||||||
|
imagePull: '镜像拉取',
|
||||||
|
imagePush: '镜像推送',
|
||||||
|
repoName: '仓库名',
|
||||||
|
imageName: '镜像名',
|
||||||
|
pull: '拉取',
|
||||||
|
path: '路径',
|
||||||
|
importImage: '导入镜像',
|
||||||
|
import: '导入',
|
||||||
|
build: '构建镜像',
|
||||||
|
label: '标签',
|
||||||
|
push: '推送',
|
||||||
|
fileName: '文件名',
|
||||||
|
export: '导出',
|
||||||
|
exportImage: '导出镜像',
|
||||||
|
version: '版本',
|
||||||
|
size: '大小',
|
||||||
|
|
||||||
repo: '仓库',
|
repo: '仓库',
|
||||||
name: '名称',
|
name: '名称',
|
||||||
downloadUrl: '下载地址',
|
downloadUrl: '下载地址',
|
||||||
|
|||||||
@@ -0,0 +1,391 @@
|
|||||||
|
<template>
|
||||||
|
<div v-loading="loading">
|
||||||
|
<el-card style="margin-top: 20px">
|
||||||
|
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
||||||
|
<template #toolbar>
|
||||||
|
<el-button type="primary" @click="pullVisiable = true">
|
||||||
|
{{ $t('container.pullFromRepo') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="loadVisiable = true">
|
||||||
|
{{ $t('container.importImage') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="onBatchDelete(null)">
|
||||||
|
{{ $t('container.build') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" plain :disabled="selects.length === 0" @click="onBatchDelete(null)">
|
||||||
|
{{ $t('commons.button.delete') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<el-table-column type="selection" fix></el-table-column>
|
||||||
|
<el-table-column label="ID" show-overflow-tooltip prop="id" min-width="60" />
|
||||||
|
<el-table-column :label="$t('commons.table.name')" show-overflow-tooltip prop="name" min-width="100" />
|
||||||
|
<el-table-column :label="$t('container.version')" prop="version" min-width="60" fix />
|
||||||
|
<el-table-column :label="$t('container.size')" prop="size" min-width="70" fix />
|
||||||
|
<el-table-column :label="$t('commons.table.createdAt')" min-width="80" fix>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ dateFromat(0, 0, row.createdAt) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" />
|
||||||
|
</ComplexTable>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog v-model="pullVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="50%">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>{{ $t('container.imagePull') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form ref="pullFormRef" :model="pullForm" label-width="80px">
|
||||||
|
<el-form-item :label="$t('container.repoName')" :rules="Rules.requiredSelect" prop="repoID">
|
||||||
|
<el-select style="width: 100%" filterable v-model="pullForm.repoID">
|
||||||
|
<el-option
|
||||||
|
v-for="item in repos"
|
||||||
|
:key="item.id"
|
||||||
|
:value="item.id"
|
||||||
|
:label="item.name + ' [ ' + item.downloadUrl + ' ] '"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('container.imageName')" :rules="Rules.requiredInput" prop="imageName">
|
||||||
|
<el-input v-model="pullForm.imageName"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="pullForm.imageName !== ''">
|
||||||
|
<el-tag>docker pull {{ loadDetailInfo(pullForm.repoID) }}/{{ pullForm.imageName }}</el-tag>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="submitPull(pullFormRef)">
|
||||||
|
{{ $t('container.pull') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="pullVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="pushVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="50%">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>{{ $t('container.imagePush') }} ({{ pushForm.imageName }})</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form ref="pushFormRef" :model="pushForm" label-width="80px">
|
||||||
|
<el-form-item :label="$t('container.repoName')" :rules="Rules.requiredSelect" prop="repoID">
|
||||||
|
<el-select style="width: 100%" filterable v-model="pushForm.repoID">
|
||||||
|
<el-option
|
||||||
|
v-for="item in repos"
|
||||||
|
:key="item.id"
|
||||||
|
:value="item.id"
|
||||||
|
:label="item.name + ' [ ' + item.downloadUrl + ' ] '"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('container.label')" :rules="Rules.requiredInput" prop="tagName">
|
||||||
|
<el-input v-model="pushForm.tagName"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="pushForm.tagName !== ''">
|
||||||
|
<el-tag>
|
||||||
|
docker tag {{ pushForm.imageName }} {{ loadDetailInfo(pushForm.repoID) }}/{{ pushForm.tagName }}
|
||||||
|
</el-tag>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="pushForm.tagName !== ''">
|
||||||
|
<el-tag>docker push {{ loadDetailInfo(pushForm.repoID) }}/{{ pushForm.tagName }}</el-tag>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="submitPush(pushFormRef)">
|
||||||
|
{{ $t('container.push') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="pushVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="saveVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="50%">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>{{ $t('container.exportImage') }} ({{ saveForm.imageName }})</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form ref="saveFormRef" :model="saveForm" label-width="80px">
|
||||||
|
<el-form-item :label="$t('container.path')" :rules="Rules.requiredSelect" prop="path">
|
||||||
|
<el-input clearable v-model="saveForm.path">
|
||||||
|
<template #append>
|
||||||
|
<FileList @choose="loadSaveDir" :dir="true"></FileList>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('container.fileName')" :rules="Rules.requiredInput" prop="name">
|
||||||
|
<el-input v-model="saveForm.name">
|
||||||
|
<template #append>.tar</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="saveForm.path !== '' && saveForm.name !== ''">
|
||||||
|
<el-tag>docker save {{ saveForm.imageName }} > {{ saveForm.path }}/{{ saveForm.name }}.tar</el-tag>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="submitSave(saveFormRef)">
|
||||||
|
{{ $t('container.export') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="saveVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="loadVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>{{ $t('container.importImage') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form ref="loadFormRef" :model="loadForm" label-width="80px">
|
||||||
|
<el-form-item :label="$t('container.path')" :rules="Rules.requiredSelect" prop="path">
|
||||||
|
<el-input clearable v-model="loadForm.path">
|
||||||
|
<template #append>
|
||||||
|
<FileList @choose="loadLoadDir" :dir="false"></FileList>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="loadForm.path !== ''">
|
||||||
|
<el-tag>docker load < {{ loadForm.path }}</el-tag>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="submitLoad(loadFormRef)">{{ $t('container.import') }}</el-button>
|
||||||
|
<el-button @click="loadVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
|
import { reactive, onMounted, ref } from 'vue';
|
||||||
|
import FileList from '@/components/file-list/index.vue';
|
||||||
|
import { dateFromat } from '@/utils/util';
|
||||||
|
import { Container } from '@/api/interface/container';
|
||||||
|
import {
|
||||||
|
getImagePage,
|
||||||
|
getRepoOption,
|
||||||
|
imageLoad,
|
||||||
|
imagePull,
|
||||||
|
imagePush,
|
||||||
|
imageRemove,
|
||||||
|
imageSave,
|
||||||
|
} from '@/api/modules/container';
|
||||||
|
import { Rules } from '@/global/form-rules';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { ElForm, ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const data = ref();
|
||||||
|
const repos = ref();
|
||||||
|
const selects = ref<any>([]);
|
||||||
|
const paginationConfig = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormInstance = InstanceType<typeof ElForm>;
|
||||||
|
const pullVisiable = ref(false);
|
||||||
|
const pullFormRef = ref<FormInstance>();
|
||||||
|
const pullForm = reactive({
|
||||||
|
repoID: 1,
|
||||||
|
imageName: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pushVisiable = ref(false);
|
||||||
|
const pushFormRef = ref<FormInstance>();
|
||||||
|
const pushForm = reactive({
|
||||||
|
repoID: 1,
|
||||||
|
imageName: '',
|
||||||
|
tagName: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveVisiable = ref(false);
|
||||||
|
const saveFormRef = ref<FormInstance>();
|
||||||
|
const saveForm = reactive({
|
||||||
|
imageName: '',
|
||||||
|
path: '',
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadVisiable = ref(false);
|
||||||
|
const loadFormRef = ref<FormInstance>();
|
||||||
|
const loadForm = reactive({
|
||||||
|
path: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
const repoSearch = {
|
||||||
|
page: paginationConfig.page,
|
||||||
|
pageSize: paginationConfig.pageSize,
|
||||||
|
};
|
||||||
|
await getImagePage(repoSearch).then((res) => {
|
||||||
|
if (res.data) {
|
||||||
|
data.value = res.data.items;
|
||||||
|
}
|
||||||
|
paginationConfig.total = res.data.total;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const loadRepos = async () => {
|
||||||
|
const res = await getRepoOption();
|
||||||
|
repos.value = res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadSaveDir = async (path: string) => {
|
||||||
|
saveForm.path = path;
|
||||||
|
};
|
||||||
|
const loadLoadDir = async (path: string) => {
|
||||||
|
loadForm.path = path;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitPull = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.validate(async (valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
pullVisiable.value = false;
|
||||||
|
await imagePull(pullForm);
|
||||||
|
loading.value = false;
|
||||||
|
search();
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
} catch {
|
||||||
|
loading.value = false;
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitPush = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.validate(async (valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
pushVisiable.value = false;
|
||||||
|
await imagePush(pushForm);
|
||||||
|
loading.value = false;
|
||||||
|
search();
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
} catch {
|
||||||
|
loading.value = false;
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitLoad = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.validate(async (valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
loadVisiable.value = false;
|
||||||
|
await imageLoad(loadForm);
|
||||||
|
loading.value = false;
|
||||||
|
search();
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
} catch {
|
||||||
|
loading.value = false;
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitSave = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.validate(async (valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
saveVisiable.value = false;
|
||||||
|
await imageSave(saveForm);
|
||||||
|
loading.value = false;
|
||||||
|
search();
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
} catch {
|
||||||
|
loading.value = false;
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBatchDelete = async (row: Container.ImageInfo | null) => {
|
||||||
|
ElMessageBox.confirm(i18n.global.t('commons.msg.delete'), i18n.global.t('commons.msg.deleteTitle'), {
|
||||||
|
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||||
|
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||||
|
type: 'info',
|
||||||
|
}).then(async () => {
|
||||||
|
if (row) {
|
||||||
|
loading.value = true;
|
||||||
|
await imageRemove({ imageName: row.name + ':' + row.version });
|
||||||
|
loading.value = false;
|
||||||
|
search();
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ps = [];
|
||||||
|
for (const item of selects.value) {
|
||||||
|
ps.push(imageRemove({ imageName: item.name + ':' + item.version }));
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
Promise.all(ps)
|
||||||
|
.then(() => {
|
||||||
|
loading.value = false;
|
||||||
|
search();
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
loading.value = false;
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadDetailInfo(id: number) {
|
||||||
|
for (const item of repos.value) {
|
||||||
|
if (item.id === id) {
|
||||||
|
return item.downloadUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttons = [
|
||||||
|
{
|
||||||
|
label: i18n.global.t('container.push'),
|
||||||
|
click: (row: Container.ImageInfo) => {
|
||||||
|
pushForm.imageName = row.name + ':' + row.version;
|
||||||
|
pushVisiable.value = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.global.t('container.export'),
|
||||||
|
click: (row: Container.ImageInfo) => {
|
||||||
|
saveForm.imageName = row.name + ':' + row.version;
|
||||||
|
saveVisiable.value = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18n.global.t('commons.button.delete'),
|
||||||
|
click: (row: Container.ImageInfo) => {
|
||||||
|
onBatchDelete(row);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
search();
|
||||||
|
loadRepos();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
<Container v-if="activeNames === 'container'" />
|
<Container v-if="activeNames === 'container'" />
|
||||||
<Repo v-if="activeNames === 'repo'" />
|
<Repo v-if="activeNames === 'repo'" />
|
||||||
<Backup v-if="activeNames === 'network'" />
|
<Image v-if="activeNames === 'image'" />
|
||||||
<Monitor v-if="activeNames === 'storage'" />
|
<Monitor v-if="activeNames === 'storage'" />
|
||||||
<About v-if="activeNames === 'schedule'" />
|
<About v-if="activeNames === 'schedule'" />
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
import { ref } from 'vue';
|
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 Backup from '@/views/setting/tabs/backup.vue';
|
import Image from '@/views/container/image/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';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user