mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-31 14:08:06 +08:00
feat: 拉取推送镜像增加实时日志
This commit is contained in:
parent
b1c2c4be83
commit
72b2633bdf
@ -71,12 +71,13 @@ func (b *BaseApi) ImagePull(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := imageService.ImagePull(req); err != nil {
|
||||
logPath, err := imageService.ImagePull(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, nil)
|
||||
helper.SuccessWithData(c, logPath)
|
||||
}
|
||||
|
||||
func (b *BaseApi) ImagePush(c *gin.Context) {
|
||||
@ -90,12 +91,13 @@ func (b *BaseApi) ImagePush(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := imageService.ImagePush(req); err != nil {
|
||||
logPath, err := imageService.ImagePush(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, nil)
|
||||
helper.SuccessWithData(c, logPath)
|
||||
}
|
||||
|
||||
func (b *BaseApi) ImageRemove(c *gin.Context) {
|
||||
|
@ -2,13 +2,13 @@ package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
@ -19,15 +19,18 @@ import (
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
)
|
||||
|
||||
const dockerLogDir = constant.TmpDir + "/docker_logs"
|
||||
|
||||
type ImageService struct{}
|
||||
|
||||
type IImageService interface {
|
||||
Page(req dto.PageInfo) (int64, interface{}, error)
|
||||
List() ([]dto.Options, error)
|
||||
ImagePull(req dto.ImagePull) error
|
||||
ImageBuild(req dto.ImageBuild) (string, error)
|
||||
ImagePull(req dto.ImagePull) (string, error)
|
||||
ImageLoad(req dto.ImageLoad) error
|
||||
ImageSave(req dto.ImageSave) error
|
||||
ImagePush(req dto.ImagePush) error
|
||||
ImagePush(req dto.ImagePush) (string, error)
|
||||
ImageRemove(req dto.BatchDelete) error
|
||||
}
|
||||
|
||||
@ -138,11 +141,13 @@ func (u *ImageService) ImageBuild(req dto.ImageBuild) (string, error) {
|
||||
}
|
||||
go func() {
|
||||
defer file.Close()
|
||||
defer tar.Close()
|
||||
res, err := client.ImageBuild(context.TODO(), tar, opts)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("build image %s failed, err: %v", req.Name, err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
global.LOG.Debugf("build image %s successful!", req.Name)
|
||||
_, _ = io.Copy(file, res.Body)
|
||||
}()
|
||||
@ -150,28 +155,38 @@ func (u *ImageService) ImageBuild(req dto.ImageBuild) (string, error) {
|
||||
return logName, nil
|
||||
}
|
||||
|
||||
func (u *ImageService) ImagePull(req dto.ImagePull) error {
|
||||
func (u *ImageService) ImagePull(req dto.ImagePull) (string, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
if _, err := os.Stat(dockerLogDir); err != nil && os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(dockerLogDir, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
path := fmt.Sprintf("%s/image_pull_%s_%s.log", dockerLogDir, strings.ReplaceAll(req.ImageName, ":", "_"), time.Now().Format("20060102150405"))
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if req.RepoID == 0 {
|
||||
go func() {
|
||||
defer file.Close()
|
||||
out, err := client.ImagePull(context.TODO(), req.ImageName, types.ImagePullOptions{})
|
||||
if err != nil {
|
||||
global.LOG.Errorf("image %s pull failed, err: %v", req.ImageName, err)
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
buf := new(bytes.Buffer)
|
||||
_, _ = buf.ReadFrom(out)
|
||||
global.LOG.Debugf("image %s pull stdout: %v", req.ImageName, buf.String())
|
||||
global.LOG.Debugf("pull image %s successful!", req.ImageName)
|
||||
_, _ = io.Copy(file, out)
|
||||
}()
|
||||
return nil
|
||||
return path, nil
|
||||
}
|
||||
repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.RepoID))
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
options := types.ImagePullOptions{}
|
||||
if repo.Auth {
|
||||
@ -181,24 +196,24 @@ func (u *ImageService) ImagePull(req dto.ImagePull) error {
|
||||
}
|
||||
encodedJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
||||
options.RegistryAuth = authStr
|
||||
}
|
||||
image := repo.DownloadUrl + "/" + req.ImageName
|
||||
go func() {
|
||||
defer file.Close()
|
||||
out, err := client.ImagePull(context.TODO(), 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())
|
||||
global.LOG.Debugf("pull image %s successful!", req.ImageName)
|
||||
_, _ = io.Copy(file, out)
|
||||
}()
|
||||
return nil
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
|
||||
@ -251,14 +266,14 @@ func (u *ImageService) ImageTag(req dto.ImageTag) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ImageService) ImagePush(req dto.ImagePush) error {
|
||||
func (u *ImageService) ImagePush(req dto.ImagePush) (string, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.RepoID))
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
options := types.ImagePushOptions{}
|
||||
if repo.Auth {
|
||||
@ -268,7 +283,7 @@ func (u *ImageService) ImagePush(req dto.ImagePush) error {
|
||||
}
|
||||
encodedJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
||||
options.RegistryAuth = authStr
|
||||
@ -276,22 +291,33 @@ func (u *ImageService) ImagePush(req dto.ImagePush) error {
|
||||
newName := fmt.Sprintf("%s/%s", repo.DownloadUrl, req.Name)
|
||||
if newName != req.TagName {
|
||||
if err := client.ImageTag(context.TODO(), req.TagName, newName); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(dockerLogDir); err != nil && os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(dockerLogDir, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
path := fmt.Sprintf("%s/image_push_%s_%s.log", dockerLogDir, strings.ReplaceAll(req.Name, ":", "_"), time.Now().Format("20060102150405"))
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
go func() {
|
||||
defer file.Close()
|
||||
out, err := client.ImagePush(context.TODO(), newName, options)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("image %s push failed, err: %v", req.TagName, err)
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
buf := new(bytes.Buffer)
|
||||
_, _ = buf.ReadFrom(out)
|
||||
global.LOG.Debugf("image %s push stdout: %v", req.TagName, buf.String())
|
||||
global.LOG.Debugf("push image %s successful!", req.Name)
|
||||
_, _ = io.Copy(file, out)
|
||||
}()
|
||||
|
||||
return nil
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (u *ImageService) ImageRemove(req dto.BatchDelete) error {
|
||||
|
@ -32,10 +32,10 @@ export const imageBuild = (params: Container.ImageBuild) => {
|
||||
return http.post<string>(`/containers/image/build`, params);
|
||||
};
|
||||
export const imagePull = (params: Container.ImagePull) => {
|
||||
return http.post(`/containers/image/pull`, params);
|
||||
return http.post<string>(`/containers/image/pull`, params);
|
||||
};
|
||||
export const imagePush = (params: Container.ImagePush) => {
|
||||
return http.post(`/containers/image/push`, params);
|
||||
return http.post<string>(`/containers/image/push`, params);
|
||||
};
|
||||
export const imageLoad = (params: Container.ImageLoad) => {
|
||||
return http.post(`/containers/image/load`, params);
|
||||
|
@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<el-dialog v-model="pullVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
|
||||
<el-dialog
|
||||
v-model="pullVisiable"
|
||||
@close="onCloseLog"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
width="50%"
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('container.imagePull') }}</span>
|
||||
@ -25,10 +31,28 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<codemirror
|
||||
v-if="logVisiable"
|
||||
:autofocus="true"
|
||||
placeholder="Wait for pull output..."
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
style="max-height: 300px"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
v-model="logInfo"
|
||||
:readOnly="true"
|
||||
/>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="pullVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="onSubmit(formRef)">
|
||||
<el-button :disabled="buttonDisabled" @click="pullVisiable = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button :disabled="buttonDisabled" type="primary" @click="onSubmit(formRef)">
|
||||
{{ $t('container.pull') }}
|
||||
</el-button>
|
||||
</span>
|
||||
@ -43,6 +67,10 @@ import i18n from '@/lang';
|
||||
import { ElForm, ElMessage } from 'element-plus';
|
||||
import { imagePull } from '@/api/modules/container';
|
||||
import { Container } from '@/api/interface/container';
|
||||
import { Codemirror } from 'vue-codemirror';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { LoadFile } from '@/api/modules/files';
|
||||
|
||||
const pullVisiable = ref(false);
|
||||
const form = reactive({
|
||||
@ -51,6 +79,13 @@ const form = reactive({
|
||||
imageName: '',
|
||||
});
|
||||
|
||||
const buttonDisabled = ref(false);
|
||||
|
||||
const logVisiable = ref(false);
|
||||
const logInfo = ref();
|
||||
const extensions = [javascript(), oneDark];
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
|
||||
interface DialogProps {
|
||||
repos: Array<Container.RepoOptions>;
|
||||
}
|
||||
@ -64,6 +99,8 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
form.repoID = 1;
|
||||
form.imageName = '';
|
||||
dialogData.value.repos = params.repos;
|
||||
buttonDisabled.value = false;
|
||||
logInfo.value = '';
|
||||
};
|
||||
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
@ -75,20 +112,31 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
try {
|
||||
if (!form.fromRepo) {
|
||||
form.repoID = 0;
|
||||
}
|
||||
pullVisiable.value = false;
|
||||
await imagePull(form);
|
||||
emit('search');
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
} catch {
|
||||
emit('search');
|
||||
if (!form.fromRepo) {
|
||||
form.repoID = 0;
|
||||
}
|
||||
const res = await imagePull(form);
|
||||
logVisiable.value = true;
|
||||
buttonDisabled.value = true;
|
||||
loadLogs(res.data);
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
});
|
||||
};
|
||||
|
||||
const loadLogs = async (path: string) => {
|
||||
timer = setInterval(async () => {
|
||||
if (logVisiable.value) {
|
||||
const res = await LoadFile({ path: path });
|
||||
logInfo.value = res.data;
|
||||
}
|
||||
}, 1000 * 3);
|
||||
};
|
||||
const onCloseLog = async () => {
|
||||
emit('search');
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
};
|
||||
|
||||
function loadDetailInfo(id: number) {
|
||||
for (const item of dialogData.value.repos) {
|
||||
if (item.id === id) {
|
||||
|
@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<el-dialog v-model="pushVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
|
||||
<el-dialog
|
||||
v-model="pushVisiable"
|
||||
@close="onCloseLog"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
width="50%"
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('container.imagePush') }}</span>
|
||||
@ -22,10 +28,28 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<codemirror
|
||||
v-if="logVisiable"
|
||||
:autofocus="true"
|
||||
placeholder="Wait for pull output..."
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
style="max-height: 300px"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
v-model="logInfo"
|
||||
:readOnly="true"
|
||||
/>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="pushVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="onSubmit(formRef)">
|
||||
<el-button :disabled="buttonDisabled" @click="pushVisiable = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button :disabled="buttonDisabled" type="primary" @click="onSubmit(formRef)">
|
||||
{{ $t('container.push') }}
|
||||
</el-button>
|
||||
</span>
|
||||
@ -40,6 +64,10 @@ import i18n from '@/lang';
|
||||
import { ElForm, ElMessage } from 'element-plus';
|
||||
import { imagePush } from '@/api/modules/container';
|
||||
import { Container } from '@/api/interface/container';
|
||||
import { Codemirror } from 'vue-codemirror';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { LoadFile } from '@/api/modules/files';
|
||||
|
||||
const pushVisiable = ref(false);
|
||||
const form = reactive({
|
||||
@ -49,6 +77,13 @@ const form = reactive({
|
||||
name: '',
|
||||
});
|
||||
|
||||
const buttonDisabled = ref(false);
|
||||
|
||||
const logVisiable = ref(false);
|
||||
const logInfo = ref();
|
||||
const extensions = [javascript(), oneDark];
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
|
||||
interface DialogProps {
|
||||
repos: Array<Container.RepoOptions>;
|
||||
tags: Array<string>;
|
||||
@ -76,17 +111,28 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
try {
|
||||
pushVisiable.value = false;
|
||||
await imagePush(form);
|
||||
emit('search');
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
} catch {
|
||||
emit('search');
|
||||
}
|
||||
const res = await imagePush(form);
|
||||
logVisiable.value = true;
|
||||
buttonDisabled.value = true;
|
||||
loadLogs(res.data);
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
});
|
||||
};
|
||||
|
||||
const loadLogs = async (path: string) => {
|
||||
timer = setInterval(async () => {
|
||||
if (logVisiable.value) {
|
||||
const res = await LoadFile({ path: path });
|
||||
logInfo.value = res.data;
|
||||
}
|
||||
}, 1000 * 3);
|
||||
};
|
||||
const onCloseLog = async () => {
|
||||
emit('search');
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
};
|
||||
|
||||
function loadDetailInfo(id: number) {
|
||||
for (const item of dialogData.value.repos) {
|
||||
if (item.id === id) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user