mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-03-13 17:24:44 +08:00
feat: 完成镜像拉取、推送、导入、导出功能
This commit is contained in:
parent
dcbf92ac12
commit
2cb1b57069
@ -15,6 +15,7 @@ var (
|
||||
groupService = service.ServiceGroupApp.GroupService
|
||||
containerService = service.ServiceGroupApp.ContainerService
|
||||
imageRepoService = service.ServiceGroupApp.ImageRepoService
|
||||
imageService = service.ServiceGroupApp.ImageService
|
||||
commandService = service.ServiceGroupApp.CommandService
|
||||
operationService = service.ServiceGroupApp.OperationService
|
||||
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"
|
||||
)
|
||||
|
||||
func (b *BaseApi) GetRepoList(c *gin.Context) {
|
||||
func (b *BaseApi) SearchRepo(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 := imageRepoService.Page(req)
|
||||
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) {
|
||||
var req dto.ImageRepoCreate
|
||||
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"`
|
||||
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 {
|
||||
Get(opts ...DBOption) (model.ImageRepo, error)
|
||||
Page(limit, offset int, opts ...DBOption) (int64, []model.ImageRepo, error)
|
||||
List(opts ...DBOption) ([]model.ImageRepo, error)
|
||||
Create(imageRepo *model.ImageRepo) error
|
||||
Update(id uint, vars map[string]interface{}) 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
|
||||
}
|
||||
|
||||
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 {
|
||||
return global.DB.Create(imageRepo).Error
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ type ServiceGroup struct {
|
||||
HostService
|
||||
BackupService
|
||||
GroupService
|
||||
ImageService
|
||||
ImageRepoService
|
||||
ContainerService
|
||||
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 {
|
||||
Page(search dto.PageInfo) (int64, interface{}, error)
|
||||
List() ([]dto.ImageRepoOption, error)
|
||||
Create(imageRepoDto dto.ImageRepoCreate) error
|
||||
Update(id uint, upMap map[string]interface{}) error
|
||||
BatchDelete(ids []uint) error
|
||||
@ -33,6 +34,19 @@ func (u *ImageRepoService) Page(search dto.PageInfo) (int64, interface{}, error)
|
||||
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 {
|
||||
imageRepo, _ := imageRepoRepo.Get(commonRepo.WithByName(imageRepoDto.RepoName))
|
||||
if imageRepo.ID != 0 {
|
||||
@ -48,9 +62,17 @@ func (u *ImageRepoService) Create(imageRepoDto dto.ImageRepoCreate) 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))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
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("/log", baseApi.ContainerLogs)
|
||||
|
||||
baRouter.POST("/repo/search", baseApi.GetRepoList)
|
||||
baRouter.POST("/repo/search", baseApi.SearchRepo)
|
||||
baRouter.PUT("/repo/:id", baseApi.UpdateRepo)
|
||||
baRouter.GET("/repo", baseApi.ListRepo)
|
||||
withRecordRouter.POST("/repo", baseApi.CreateRepo)
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
name: string;
|
||||
downloadUrl: string;
|
||||
@ -47,4 +75,9 @@ export namespace Container {
|
||||
password: string;
|
||||
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}`);
|
||||
};
|
||||
|
||||
// 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
|
||||
export const getRepoPage = (params: ReqPage) => {
|
||||
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) => {
|
||||
return http.post(`/containers/repo`, params);
|
||||
};
|
||||
|
@ -161,7 +161,6 @@ export default {
|
||||
reName: 'ReName',
|
||||
remove: 'Remove',
|
||||
container: 'Container',
|
||||
image: 'Image',
|
||||
network: 'Network',
|
||||
storage: 'Storage',
|
||||
schedule: 'Schedule',
|
||||
@ -172,6 +171,25 @@ export default {
|
||||
lastHour: 'Last Hour',
|
||||
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',
|
||||
name: 'Name',
|
||||
downloadUrl: 'Download URL',
|
||||
|
@ -158,7 +158,6 @@ export default {
|
||||
reName: '重命名',
|
||||
remove: '移除',
|
||||
container: '容器',
|
||||
image: '镜像',
|
||||
network: '网络',
|
||||
storage: '数据卷',
|
||||
schedule: '编排',
|
||||
@ -169,6 +168,25 @@ export default {
|
||||
lastHour: '最近 1 小时',
|
||||
last10Min: '最近 10 分钟',
|
||||
|
||||
image: '镜像',
|
||||
pullFromRepo: '从仓库中拉取',
|
||||
imagePull: '镜像拉取',
|
||||
imagePush: '镜像推送',
|
||||
repoName: '仓库名',
|
||||
imageName: '镜像名',
|
||||
pull: '拉取',
|
||||
path: '路径',
|
||||
importImage: '导入镜像',
|
||||
import: '导入',
|
||||
build: '构建镜像',
|
||||
label: '标签',
|
||||
push: '推送',
|
||||
fileName: '文件名',
|
||||
export: '导出',
|
||||
exportImage: '导出镜像',
|
||||
version: '版本',
|
||||
size: '大小',
|
||||
|
||||
repo: '仓库',
|
||||
name: '名称',
|
||||
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>
|
||||
<Container v-if="activeNames === 'container'" />
|
||||
<Repo v-if="activeNames === 'repo'" />
|
||||
<Backup v-if="activeNames === 'network'" />
|
||||
<Image v-if="activeNames === 'image'" />
|
||||
<Monitor v-if="activeNames === 'storage'" />
|
||||
<About v-if="activeNames === 'schedule'" />
|
||||
</div>
|
||||
@ -34,7 +34,7 @@
|
||||
import { ref } from 'vue';
|
||||
import Container from '@/views/container/container/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 About from '@/views/setting/tabs/about.vue';
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user