1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-03-13 17:24:44 +08:00

feat: 完成镜像拉取、推送、导入、导出功能

This commit is contained in:
ssongliu 2022-10-10 15:14:49 +08:00 committed by ssongliu
parent dcbf92ac12
commit 2cb1b57069
17 changed files with 968 additions and 6 deletions

View File

@ -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
View 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)
}

View File

@ -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
View 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"`
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -7,6 +7,7 @@ type ServiceGroup struct {
HostService
BackupService
GroupService
ImageService
ImageRepoService
ContainerService
CommandService

View 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))
}
}

View File

@ -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)
}

View 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)
}
}

View File

@ -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)
}
}

View File

@ -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;
}
}

View File

@ -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);
};

View File

@ -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',

View File

@ -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: '下载地址',

View File

@ -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 &lt; {{ 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>

View File

@ -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';