1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-31 22:18:07 +08:00

feat: 拉取推送镜像增加实时日志

This commit is contained in:
ssongliu 2022-12-07 14:28:11 +08:00 committed by ssongliu
parent b1c2c4be83
commit 72b2633bdf
5 changed files with 176 additions and 54 deletions

View File

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

View File

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

View File

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

View File

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

View File

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