feat: Support batch image pulling (#10532)

Refs #10521
This commit is contained in:
ssongliu 2025-09-29 14:18:20 +08:00 committed by GitHub
parent 129e864975
commit 4eb264bea4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 63 additions and 24 deletions

View file

@ -30,9 +30,9 @@ type ImageBuild struct {
}
type ImagePull struct {
TaskID string `json:"taskID"`
RepoID uint `json:"repoID"`
ImageName string `json:"imageName" validate:"required"`
TaskID string `json:"taskID"`
RepoID uint `json:"repoID"`
ImageName []string `json:"imageName" validate:"required"`
}
type ImageTag struct {

View file

@ -256,17 +256,19 @@ func (u *ImageService) ImagePull(req dto.ImagePull) error {
return err
}
defer client.Close()
imageItemName := strings.ReplaceAll(path.Base(req.ImageName), ":", "_")
taskItem, err := task.NewTaskWithOps(imageItemName, task.TaskPull, task.TaskScopeImage, req.TaskID, 1)
taskItem, err := task.NewTaskWithOps(strings.Join(req.ImageName, ","), task.TaskPull, task.TaskScopeImage, req.TaskID, 1)
if err != nil {
return fmt.Errorf("new task for image pull failed, err: %v", err)
}
go func() {
taskItem.AddSubTask(i18n.GetWithName("ImagePull", req.ImageName), func(t *task.Task) error {
for _, item := range req.ImageName {
itemName := strings.ReplaceAll(path.Base(item), ":", "_")
taskItem.AddSubTask(i18n.GetWithName("ImagePull", itemName), func(t *task.Task) error {
taskItem.Logf("----------------- %s -----------------", itemName)
options := image.PullOptions{}
imageName := req.ImageName
imageName := item
if req.RepoID == 0 {
hasAuth, authStr := loadAuthInfo(req.ImageName)
hasAuth, authStr := loadAuthInfo(item)
if hasAuth {
options.RegistryAuth = authStr
}
@ -288,7 +290,7 @@ func (u *ImageService) ImagePull(req dto.ImagePull) error {
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
options.RegistryAuth = authStr
}
imageName = repo.DownloadUrl + "/" + req.ImageName
imageName = repo.DownloadUrl + "/" + item
}
dockerCli := docker.NewClientWithExist(client)
err = dockerCli.PullImageWithProcessAndOptions(taskItem, imageName, options)
@ -298,6 +300,8 @@ func (u *ImageService) ImagePull(req dto.ImagePull) error {
}
return nil
}, nil)
}
go func() {
_ = taskItem.Execute()
}()
return nil

View file

@ -187,7 +187,7 @@ export namespace Container {
export interface ImagePull {
taskID: string;
repoID: number;
imageName: string;
imageName: Array<string>;
}
export interface ImageTag {
sourceID: string;

View file

@ -875,6 +875,8 @@ const message = {
image: 'Image | Images',
imagePull: 'Pull',
imagePullHelper:
'Supports selecting multiple images to pull, press Enter after entering each image to continue',
imagePush: 'Push',
imagePushHelper:
'Detected that this image has multiple tags. Please confirm that the image name used for pushing is: {0}',

View file

@ -877,6 +877,8 @@ const message = {
cache: 'Caché',
image: 'Imagen | Imágenes',
imagePull: 'Descargar',
imagePullHelper:
'Admite seleccionar múltiples imágenes para descargar, presione Enter después de ingresar cada imagen para continuar',
imagePush: 'Subir',
imagePushHelper:
'Detected that this image has multiple tags. Please confirm that the image name used for pushing is: {0}',

View file

@ -852,6 +852,7 @@ const message = {
image: '画像|画像',
imagePull: '引く',
imagePullHelper: '複数のイメージの選択をサポートし各イメージ入力後にEnterキーを押して続行します',
imagePush: '押す',
imagePushHelper:
'このイメージに複数のタグが存在することが検出されましたプッシュ時に使用するイメージ名が以下であることを確認してください{0}',

View file

@ -844,6 +844,7 @@ const message = {
cache: '캐시',
image: '이미지 | 이미지들',
imagePull: '풀',
imagePullHelper: '여러 이미지 선택 풀링을 지원하며, 이미지 입력 Enter 키를 눌러 계속합니다',
imagePush: '푸시',
imagePushHelper:
' 이미지에 여러 태그가 있는 것으로 감지되었습니다. 푸시 사용할 이미지 이름이 다음인지 확인하세요: {0}',

View file

@ -868,6 +868,8 @@ const message = {
image: 'Imej | Imej-imej',
imagePull: 'Tarik',
imagePullHelper:
'Menyokong pemilihan berbilang imej untuk ditarik, tekan Enter selepas memasukkan setiap imej untuk teruskan',
imagePush: 'Tekan',
imagePushHelper:
'Terdapat pengesahan bahawa imej ini mempunyai beberapa tag. Sila pastikan nama imej yang digunakan untuk menolak adalah: {0}',

View file

@ -863,6 +863,8 @@ const message = {
cache: 'Cache',
image: 'Imagem | Imagens',
imagePull: 'Puxar',
imagePullHelper:
'Suporta selecionar múltiplas imagens para puxar, pressione Enter após inserir cada imagem para continuar',
imagePush: 'Enviar',
imagePushHelper:
'Detectado que esta imagem possui múltiplas tags. Por favor, confirme que o nome da imagem usada para push é: {0}',

View file

@ -865,6 +865,8 @@ const message = {
image: 'Образ | Образы',
imagePull: 'Загрузить',
imagePullHelper:
'Поддерживает выбор нескольких образов для загрузки, нажмите Enter после ввода каждого образа для продолжения',
imagePush: 'Отправить',
imagePushHelper:
'Обнаружено, что у этого образа несколько тегов. Подтвердите, что имя образа, используемое для отправки: {0}',

View file

@ -884,6 +884,8 @@ const message = {
image: 'İmaj | İmajlar',
imagePull: 'Çek',
imagePullHelper:
'Birden fazla görüntü seçmeyi destekler, her görüntü girdikten sonra Entera basarak devam edin',
imagePush: 'Gönder',
imagePushHelper:
'Bu imgenin birden fazla etiketi olduğu tespit edildi. Lütfen gönderimde kullanılan imge adının şu olduğunu onaylayın: {0}',

View file

@ -840,6 +840,7 @@ const message = {
image: '鏡像',
imagePull: '拉取鏡像',
imagePullHelper: '支援選擇拉取多個鏡像輸入一組鏡像後回車繼續',
imagePush: '推送鏡像',
imagePushHelper: '檢測到該映像存在多個標籤請確認推送時使用的映像名稱為{0}',
imageDelete: '刪除鏡像',

View file

@ -839,6 +839,7 @@ const message = {
image: '镜像',
imagePull: '拉取镜像',
imagePullHelper: '支持选择拉取多个镜像输入一组镜像后回车继续',
imagePush: '推送镜像',
imagePushHelper: '检测到该镜像存在多个标签请确认推送时使用的镜像名称为{0}',
imageDelete: '删除镜像',

View file

@ -1,25 +1,21 @@
<template>
<DrawerPro v-model="drawerVisible" :header="$t('container.imagePull')" @close="onCloseLog" size="large">
<el-form ref="formRef" label-position="top" :model="form">
<el-form ref="formRef" label-position="top" :model="form" :rules="rules">
<el-form-item :label="$t('app.source')">
<el-checkbox v-model="form.fromRepo">
{{ $t('container.imageRepo') }}
</el-checkbox>
</el-form-item>
<el-form-item
v-if="form.fromRepo"
:label="$t('container.repoName')"
:rules="Rules.requiredSelect"
prop="repoID"
>
<el-form-item v-if="form.fromRepo" :label="$t('container.repoName')" prop="repoID">
<el-select clearable style="width: 100%" filterable v-model="form.repoID">
<el-option v-for="item in repos" :key="item.id" :value="item.id" :label="item.name" />
</el-select>
</el-form-item>
<el-form-item :label="$t('container.imageName')" :rules="Rules.imageName" prop="imageName">
<el-input v-model.trim="form.imageName">
<template v-if="form.fromRepo" #prepend>{{ loadDetailInfo(form.repoID) }}/</template>
</el-input>
<el-form-item :label="$t('container.imageName')" prop="imageName">
<el-input-tag v-model="form.imageName">
<template v-if="form.fromRepo" #prefix>{{ loadDetailInfo(form.repoID) }}/</template>
</el-input-tag>
<span class="input-help">{{ $t('container.imagePullHelper') }}</span>
</el-form-item>
</el-form>
<template #footer>
@ -52,8 +48,31 @@ const form = reactive({
taskID: '',
fromRepo: true,
repoID: null as number,
imageName: '',
imageName: [],
});
const verifyImage = (rule: any, value: any, callback: any) => {
if (!value || value.length === 0) {
callback(new Error(i18n.global.t('commons.rule.requiredInput')));
return;
}
for (const item of value) {
if (item === '' || typeof item === 'undefined' || item == null) {
callback(new Error(i18n.global.t('commons.rule.imageName')));
} else {
const reg = /^[a-zA-Z0-9]{1}[a-z:@A-Z0-9_/.-]{0,255}$/;
if (!reg.test(item) && item !== '') {
callback(new Error(i18n.global.t('commons.rule.imageName')));
return;
}
}
}
callback();
};
const rules = reactive({
imageName: [{ validator: verifyImage, trigger: 'blur', required: true }],
repoID: Rules.requiredSelect,
});
const taskLogRef = ref();
interface DialogProps {
@ -64,7 +83,7 @@ const repos = ref();
const acceptParams = async (params: DialogProps): Promise<void> => {
drawerVisible.value = true;
form.fromRepo = true;
form.imageName = '';
form.imageName = [];
repos.value = params.repos;
form.repoID = 1;
for (const item of repos.value) {