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 return
} }
if err := imageService.ImagePull(req); err != nil { logPath, err := imageService.ImagePull(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return
} }
helper.SuccessWithData(c, nil) helper.SuccessWithData(c, logPath)
} }
func (b *BaseApi) ImagePush(c *gin.Context) { func (b *BaseApi) ImagePush(c *gin.Context) {
@ -90,12 +91,13 @@ func (b *BaseApi) ImagePush(c *gin.Context) {
return return
} }
if err := imageService.ImagePush(req); err != nil { logPath, err := imageService.ImagePush(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return
} }
helper.SuccessWithData(c, nil) helper.SuccessWithData(c, logPath)
} }
func (b *BaseApi) ImageRemove(c *gin.Context) { func (b *BaseApi) ImageRemove(c *gin.Context) {

View File

@ -2,13 +2,13 @@ package service
import ( import (
"bufio" "bufio"
"bytes"
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"os" "os"
"strings"
"time" "time"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
@ -19,15 +19,18 @@ import (
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
) )
const dockerLogDir = constant.TmpDir + "/docker_logs"
type ImageService struct{} type ImageService struct{}
type IImageService interface { type IImageService interface {
Page(req dto.PageInfo) (int64, interface{}, error) Page(req dto.PageInfo) (int64, interface{}, error)
List() ([]dto.Options, 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 ImageLoad(req dto.ImageLoad) error
ImageSave(req dto.ImageSave) error ImageSave(req dto.ImageSave) error
ImagePush(req dto.ImagePush) error ImagePush(req dto.ImagePush) (string, error)
ImageRemove(req dto.BatchDelete) error ImageRemove(req dto.BatchDelete) error
} }
@ -138,11 +141,13 @@ func (u *ImageService) ImageBuild(req dto.ImageBuild) (string, error) {
} }
go func() { go func() {
defer file.Close() defer file.Close()
defer tar.Close()
res, err := client.ImageBuild(context.TODO(), tar, opts) res, err := client.ImageBuild(context.TODO(), tar, opts)
if err != nil { if err != nil {
global.LOG.Errorf("build image %s failed, err: %v", req.Name, err) global.LOG.Errorf("build image %s failed, err: %v", req.Name, err)
return return
} }
defer res.Body.Close()
global.LOG.Debugf("build image %s successful!", req.Name) global.LOG.Debugf("build image %s successful!", req.Name)
_, _ = io.Copy(file, res.Body) _, _ = io.Copy(file, res.Body)
}() }()
@ -150,28 +155,38 @@ func (u *ImageService) ImageBuild(req dto.ImageBuild) (string, error) {
return logName, nil return logName, nil
} }
func (u *ImageService) ImagePull(req dto.ImagePull) error { func (u *ImageService) ImagePull(req dto.ImagePull) (string, error) {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { 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 { if req.RepoID == 0 {
go func() { go func() {
defer file.Close()
out, err := client.ImagePull(context.TODO(), req.ImageName, types.ImagePullOptions{}) out, err := client.ImagePull(context.TODO(), req.ImageName, types.ImagePullOptions{})
if err != nil { if err != nil {
global.LOG.Errorf("image %s pull failed, err: %v", req.ImageName, err) global.LOG.Errorf("image %s pull failed, err: %v", req.ImageName, err)
return return
} }
defer out.Close() defer out.Close()
buf := new(bytes.Buffer) global.LOG.Debugf("pull image %s successful!", req.ImageName)
_, _ = buf.ReadFrom(out) _, _ = io.Copy(file, out)
global.LOG.Debugf("image %s pull stdout: %v", req.ImageName, buf.String())
}() }()
return nil return path, nil
} }
repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.RepoID)) repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.RepoID))
if err != nil { if err != nil {
return err return "", err
} }
options := types.ImagePullOptions{} options := types.ImagePullOptions{}
if repo.Auth { if repo.Auth {
@ -181,24 +196,24 @@ func (u *ImageService) ImagePull(req dto.ImagePull) error {
} }
encodedJSON, err := json.Marshal(authConfig) encodedJSON, err := json.Marshal(authConfig)
if err != nil { if err != nil {
return err return "", err
} }
authStr := base64.URLEncoding.EncodeToString(encodedJSON) authStr := base64.URLEncoding.EncodeToString(encodedJSON)
options.RegistryAuth = authStr options.RegistryAuth = authStr
} }
image := repo.DownloadUrl + "/" + req.ImageName image := repo.DownloadUrl + "/" + req.ImageName
go func() { go func() {
defer file.Close()
out, err := client.ImagePull(context.TODO(), image, options) out, err := client.ImagePull(context.TODO(), image, options)
if err != nil { if err != nil {
global.LOG.Errorf("image %s pull failed, err: %v", image, err) global.LOG.Errorf("image %s pull failed, err: %v", image, err)
return return
} }
defer out.Close() defer out.Close()
buf := new(bytes.Buffer) global.LOG.Debugf("pull image %s successful!", req.ImageName)
_, _ = buf.ReadFrom(out) _, _ = io.Copy(file, out)
global.LOG.Debugf("image %s pull stdout: %v", image, buf.String())
}() }()
return nil return path, nil
} }
func (u *ImageService) ImageLoad(req dto.ImageLoad) error { func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
@ -251,14 +266,14 @@ func (u *ImageService) ImageTag(req dto.ImageTag) error {
return nil return nil
} }
func (u *ImageService) ImagePush(req dto.ImagePush) error { func (u *ImageService) ImagePush(req dto.ImagePush) (string, error) {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
return err return "", err
} }
repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.RepoID)) repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.RepoID))
if err != nil { if err != nil {
return err return "", err
} }
options := types.ImagePushOptions{} options := types.ImagePushOptions{}
if repo.Auth { if repo.Auth {
@ -268,7 +283,7 @@ func (u *ImageService) ImagePush(req dto.ImagePush) error {
} }
encodedJSON, err := json.Marshal(authConfig) encodedJSON, err := json.Marshal(authConfig)
if err != nil { if err != nil {
return err return "", err
} }
authStr := base64.URLEncoding.EncodeToString(encodedJSON) authStr := base64.URLEncoding.EncodeToString(encodedJSON)
options.RegistryAuth = authStr options.RegistryAuth = authStr
@ -276,22 +291,33 @@ func (u *ImageService) ImagePush(req dto.ImagePush) error {
newName := fmt.Sprintf("%s/%s", repo.DownloadUrl, req.Name) newName := fmt.Sprintf("%s/%s", repo.DownloadUrl, req.Name)
if newName != req.TagName { if newName != req.TagName {
if err := client.ImageTag(context.TODO(), req.TagName, newName); err != nil { 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() { go func() {
defer file.Close()
out, err := client.ImagePush(context.TODO(), newName, options) out, err := client.ImagePush(context.TODO(), newName, options)
if err != nil { if err != nil {
global.LOG.Errorf("image %s push failed, err: %v", req.TagName, err) global.LOG.Errorf("image %s push failed, err: %v", req.TagName, err)
return return
} }
defer out.Close() defer out.Close()
buf := new(bytes.Buffer) global.LOG.Debugf("push image %s successful!", req.Name)
_, _ = buf.ReadFrom(out) _, _ = io.Copy(file, out)
global.LOG.Debugf("image %s push stdout: %v", req.TagName, buf.String())
}() }()
return nil return path, nil
} }
func (u *ImageService) ImageRemove(req dto.BatchDelete) error { 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); return http.post<string>(`/containers/image/build`, params);
}; };
export const imagePull = (params: Container.ImagePull) => { 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) => { 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) => { export const imageLoad = (params: Container.ImageLoad) => {
return http.post(`/containers/image/load`, params); return http.post(`/containers/image/load`, params);

View File

@ -1,5 +1,11 @@
<template> <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> <template #header>
<div class="card-header"> <div class="card-header">
<span>{{ $t('container.imagePull') }}</span> <span>{{ $t('container.imagePull') }}</span>
@ -25,10 +31,28 @@
</el-input> </el-input>
</el-form-item> </el-form-item>
</el-form> </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> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="pullVisiable = false">{{ $t('commons.button.cancel') }}</el-button> <el-button :disabled="buttonDisabled" @click="pullVisiable = false">
<el-button type="primary" @click="onSubmit(formRef)"> {{ $t('commons.button.cancel') }}
</el-button>
<el-button :disabled="buttonDisabled" type="primary" @click="onSubmit(formRef)">
{{ $t('container.pull') }} {{ $t('container.pull') }}
</el-button> </el-button>
</span> </span>
@ -43,6 +67,10 @@ import i18n from '@/lang';
import { ElForm, ElMessage } from 'element-plus'; import { ElForm, ElMessage } from 'element-plus';
import { imagePull } from '@/api/modules/container'; import { imagePull } from '@/api/modules/container';
import { Container } from '@/api/interface/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 pullVisiable = ref(false);
const form = reactive({ const form = reactive({
@ -51,6 +79,13 @@ const form = reactive({
imageName: '', imageName: '',
}); });
const buttonDisabled = ref(false);
const logVisiable = ref(false);
const logInfo = ref();
const extensions = [javascript(), oneDark];
let timer: NodeJS.Timer | null = null;
interface DialogProps { interface DialogProps {
repos: Array<Container.RepoOptions>; repos: Array<Container.RepoOptions>;
} }
@ -64,6 +99,8 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
form.repoID = 1; form.repoID = 1;
form.imageName = ''; form.imageName = '';
dialogData.value.repos = params.repos; dialogData.value.repos = params.repos;
buttonDisabled.value = false;
logInfo.value = '';
}; };
const emit = defineEmits<{ (e: 'search'): void }>(); const emit = defineEmits<{ (e: 'search'): void }>();
@ -75,20 +112,31 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;
formEl.validate(async (valid) => { formEl.validate(async (valid) => {
if (!valid) return; if (!valid) return;
try { if (!form.fromRepo) {
if (!form.fromRepo) { form.repoID = 0;
form.repoID = 0;
}
pullVisiable.value = false;
await imagePull(form);
emit('search');
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
} catch {
emit('search');
} }
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) { function loadDetailInfo(id: number) {
for (const item of dialogData.value.repos) { for (const item of dialogData.value.repos) {
if (item.id === id) { if (item.id === id) {

View File

@ -1,5 +1,11 @@
<template> <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> <template #header>
<div class="card-header"> <div class="card-header">
<span>{{ $t('container.imagePush') }}</span> <span>{{ $t('container.imagePush') }}</span>
@ -22,10 +28,28 @@
</el-input> </el-input>
</el-form-item> </el-form-item>
</el-form> </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> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="pushVisiable = false">{{ $t('commons.button.cancel') }}</el-button> <el-button :disabled="buttonDisabled" @click="pushVisiable = false">
<el-button type="primary" @click="onSubmit(formRef)"> {{ $t('commons.button.cancel') }}
</el-button>
<el-button :disabled="buttonDisabled" type="primary" @click="onSubmit(formRef)">
{{ $t('container.push') }} {{ $t('container.push') }}
</el-button> </el-button>
</span> </span>
@ -40,6 +64,10 @@ import i18n from '@/lang';
import { ElForm, ElMessage } from 'element-plus'; import { ElForm, ElMessage } from 'element-plus';
import { imagePush } from '@/api/modules/container'; import { imagePush } from '@/api/modules/container';
import { Container } from '@/api/interface/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 pushVisiable = ref(false);
const form = reactive({ const form = reactive({
@ -49,6 +77,13 @@ const form = reactive({
name: '', name: '',
}); });
const buttonDisabled = ref(false);
const logVisiable = ref(false);
const logInfo = ref();
const extensions = [javascript(), oneDark];
let timer: NodeJS.Timer | null = null;
interface DialogProps { interface DialogProps {
repos: Array<Container.RepoOptions>; repos: Array<Container.RepoOptions>;
tags: Array<string>; tags: Array<string>;
@ -76,17 +111,28 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;
formEl.validate(async (valid) => { formEl.validate(async (valid) => {
if (!valid) return; if (!valid) return;
try { const res = await imagePush(form);
pushVisiable.value = false; logVisiable.value = true;
await imagePush(form); buttonDisabled.value = true;
emit('search'); loadLogs(res.data);
ElMessage.success(i18n.global.t('commons.msg.operationSuccess')); ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
} catch {
emit('search');
}
}); });
}; };
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) { function loadDetailInfo(id: number) {
for (const item of dialogData.value.repos) { for (const item of dialogData.value.repos) {
if (item.id === id) { if (item.id === id) {