feat: Support batch upgrades for container images (#9915)

This commit is contained in:
ssongliu 2025-08-08 20:29:00 +08:00 committed by GitHub
parent c49c16cbb5
commit b156ef9476
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 206 additions and 53 deletions

View file

@ -57,7 +57,7 @@ func (b *BaseApi) LoadContainerUsers(c *gin.Context) {
// @Summary List containers // @Summary List containers
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {array} string // @Success 200 {array} dto.ContainerOptions
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /containers/list [post] // @Router /containers/list [post]
@ -65,6 +65,23 @@ func (b *BaseApi) ListContainer(c *gin.Context) {
helper.SuccessWithData(c, containerService.List()) helper.SuccessWithData(c, containerService.List())
} }
// @Tags Container
// @Summary List containers by image
// @Accept json
// @Produce json
// @Success 200 {array} dto.ContainerOptions
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/list/byimage [post]
func (b *BaseApi) ListContainerByImage(c *gin.Context) {
var req dto.OperationWithName
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
helper.SuccessWithData(c, containerService.ListByImage(req.Name))
}
// @Tags Container // @Tags Container
// @Summary Load containers status // @Summary Load containers status
// @Accept json // @Accept json

View file

@ -107,10 +107,10 @@ type ContainerCreateByCommand struct {
} }
type ContainerUpgrade struct { type ContainerUpgrade struct {
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
Name string `json:"name" validate:"required"` Names []string `json:"names" validate:"required"`
Image string `json:"image" validate:"required"` Image string `json:"image" validate:"required"`
ForcePull bool `json:"forcePull"` ForcePull bool `json:"forcePull"`
} }
type ContainerListStats struct { type ContainerListStats struct {

View file

@ -54,6 +54,7 @@ type ContainerService struct{}
type IContainerService interface { type IContainerService interface {
Page(req dto.PageContainer) (int64, interface{}, error) Page(req dto.PageContainer) (int64, interface{}, error)
List() []dto.ContainerOptions List() []dto.ContainerOptions
ListByImage(imageName string) []dto.ContainerOptions
LoadStatus() (dto.ContainerStatus, error) LoadStatus() (dto.ContainerStatus, error)
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
ListNetwork() ([]dto.Options, error) ListNetwork() ([]dto.Options, error)
@ -246,6 +247,33 @@ func (u *ContainerService) List() []dto.ContainerOptions {
return options return options
} }
func (u *ContainerService) ListByImage(imageName string) []dto.ContainerOptions {
var options []dto.ContainerOptions
client, err := docker.NewDockerClient()
if err != nil {
global.LOG.Errorf("load docker client for contianer list failed, err: %v", err)
return nil
}
defer client.Close()
containers, err := client.ContainerList(context.Background(), container.ListOptions{All: true})
if err != nil {
global.LOG.Errorf("load container list failed, err: %v", err)
return nil
}
for _, container := range containers {
if container.Image != imageName {
continue
}
for _, name := range container.Names {
if len(name) != 0 {
options = append(options, dto.ContainerOptions{Name: strings.TrimPrefix(name, "/"), State: container.State})
}
}
}
return options
}
func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) { func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
var data dto.ContainerStatus var data dto.ContainerStatus
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
@ -683,6 +711,12 @@ func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
return err return err
} }
if len(oldContainer.Config.Entrypoint) != 0 {
if oldContainer.Config.Entrypoint[0] == "/docker-entrypoint.sh" {
oldContainer.Config.Entrypoint = []string{}
}
}
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpdate, task.TaskScopeContainer, req.TaskID, 1) taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpdate, task.TaskScopeContainer, req.TaskID, 1)
if err != nil { if err != nil {
global.LOG.Errorf("new task for create container failed, err: %v", err) global.LOG.Errorf("new task for create container failed, err: %v", err)
@ -745,17 +779,14 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
} }
defer client.Close() defer client.Close()
ctx := context.Background() ctx := context.Background()
oldContainer, err := client.ContainerInspect(ctx, req.Name) taskItem, err := task.NewTaskWithOps(req.Image, task.TaskUpgrade, task.TaskScopeImage, req.TaskID, 1)
if err != nil {
return err
}
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpgrade, task.TaskScopeContainer, req.TaskID, 1)
if err != nil { if err != nil {
global.LOG.Errorf("new task for create container failed, err: %v", err) global.LOG.Errorf("new task for create container failed, err: %v", err)
return err return err
} }
go func() { go func() {
taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error { taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error {
taskItem.LogStart(i18n.GetWithName("ContainerImagePull", req.Image))
if !checkImageExist(client, req.Image) || req.ForcePull { if !checkImageExist(client, req.Image) || req.ForcePull {
if err := pullImages(taskItem, client, req.Image); err != nil { if err := pullImages(taskItem, client, req.Image); err != nil {
if !req.ForcePull { if !req.ForcePull {
@ -766,38 +797,58 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
} }
return nil return nil
}, nil) }, nil)
for _, item := range req.Names {
taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", req.Name), func(t *task.Task) error { var oldContainer container.InspectResponse
config := oldContainer.Config taskItem.AddSubTask(i18n.GetWithName("ContainerLoadInfo", item), func(t *task.Task) error {
config.Image = req.Image taskItem.Logf("----------------- %s -----------------", item)
hostConf := oldContainer.HostConfig oldContainer, err = client.ContainerInspect(ctx, item)
var networkConf network.NetworkingConfig if err != nil {
if oldContainer.NetworkSettings != nil { return err
for networkKey := range oldContainer.NetworkSettings.Networks {
networkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
break
} }
} inspected, err := client.ImageInspect(ctx, req.Image)
err := client.ContainerRemove(ctx, req.Name, container.RemoveOptions{Force: true}) if err != nil {
taskItem.LogWithStatus(i18n.GetWithName("ContainerRemoveOld", req.Name), err) return fmt.Errorf("inspect image failed, err: %v", err)
if err != nil { }
return err if isDynamicImage(inspected) {
} oldContainer.Config.Entrypoint = nil
oldContainer.Config.Cmd = nil
}
return nil
}, nil)
con, err := client.ContainerCreate(ctx, config, hostConf, &networkConf, &v1.Platform{}, req.Name) taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", item), func(t *task.Task) error {
if err != nil { oldContainer.Config.Cmd = nil
taskItem.Log(i18n.GetMsgByKey("ContainerRecreate")) config := oldContainer.Config
reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings) config.Image = req.Image
return fmt.Errorf("upgrade container failed, err: %v", err) hostConf := oldContainer.HostConfig
} var networkConf network.NetworkingConfig
err = client.ContainerStart(ctx, con.ID, container.StartOptions{}) if oldContainer.NetworkSettings != nil {
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err) for networkKey := range oldContainer.NetworkSettings.Networks {
if err != nil { networkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
return fmt.Errorf("upgrade successful but start failed, err: %v", err) break
} }
return nil }
}, nil) err := client.ContainerRemove(ctx, item, container.RemoveOptions{Force: true})
taskItem.LogWithStatus(i18n.GetWithName("ContainerRemoveOld", item), err)
if err != nil {
return err
}
con, err := client.ContainerCreate(ctx, config, hostConf, &networkConf, &v1.Platform{}, item)
if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerRecreate"))
reCreateAfterUpdate(item, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings)
return fmt.Errorf("upgrade container failed, err: %v", err)
}
err = client.ContainerStart(ctx, con.ID, container.StartOptions{})
taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err)
if err != nil {
return fmt.Errorf("upgrade successful but start failed, err: %v", err)
}
return nil
}, nil)
}
if err := taskItem.Execute(); err != nil { if err := taskItem.Execute(); err != nil {
global.LOG.Error(err.Error()) global.LOG.Error(err.Error())
} }
@ -1692,3 +1743,23 @@ func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper {
} }
return exposedPorts return exposedPorts
} }
func isDynamicImage(inspected image.InspectResponse) bool {
if len(inspected.Config.Entrypoint) > 0 {
entrypointStr := strings.Join(inspected.Config.Entrypoint, " ")
if strings.Contains(entrypointStr, "entrypoint") {
return true
}
}
dirs := []string{"/docker-entrypoint.d", "/docker-entrypoint-initdb.d"}
for _, dir := range dirs {
for _, layer := range inspected.RootFS.Layers {
if strings.Contains(layer, dir) {
return true
}
}
}
return false
}

View file

@ -21,6 +21,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.POST("/info", baseApi.ContainerInfo) baRouter.POST("/info", baseApi.ContainerInfo)
baRouter.POST("/search", baseApi.SearchContainer) baRouter.POST("/search", baseApi.SearchContainer)
baRouter.POST("/list", baseApi.ListContainer) baRouter.POST("/list", baseApi.ListContainer)
baRouter.POST("/list/byimage", baseApi.ListContainerByImage)
baRouter.GET("/status", baseApi.LoadContainerStatus) baRouter.GET("/status", baseApi.LoadContainerStatus)
baRouter.GET("/list/stats", baseApi.ContainerListStats) baRouter.GET("/list/stats", baseApi.ContainerListStats)
baRouter.GET("/search/log", baseApi.ContainerStreamLogs) baRouter.GET("/search/log", baseApi.ContainerStreamLogs)

View file

@ -45,6 +45,10 @@ export namespace Container {
imageSize: number; imageSize: number;
} }
export interface ContainerOption {
name: string;
state: string;
}
export interface ResourceLimit { export interface ResourceLimit {
cpu: number; cpu: number;
memory: number; memory: number;
@ -86,7 +90,7 @@ export namespace Container {
} }
export interface ContainerUpgrade { export interface ContainerUpgrade {
taskID: string; taskID: string;
name: string; names: Array<string>;
image: string; image: string;
forcePull: boolean; forcePull: boolean;
} }

View file

@ -7,7 +7,10 @@ export const searchContainer = (params: Container.ContainerSearch) => {
return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params, TimeoutEnum.T_40S); return http.post<ResPage<Container.ContainerInfo>>(`/containers/search`, params, TimeoutEnum.T_40S);
}; };
export const listContainer = () => { export const listContainer = () => {
return http.post<Array<Container.ContainerInfo>>(`/containers/list`, {}); return http.post<Array<Container.ContainerOption>>(`/containers/list`, {});
};
export const listContainerByImage = (image: string) => {
return http.post<Array<Container.ContainerOption>>(`/containers/list/byimage`, { name: image });
}; };
export const loadContainerUsers = (name: string) => { export const loadContainerUsers = (name: string) => {
return http.post<Array<string>>(`/containers/users`, { name: name }); return http.post<Array<string>>(`/containers/users`, { name: name });

View file

@ -805,6 +805,8 @@ const message = {
upgradeWarning2: upgradeWarning2:
'The upgrade operation requires rebuilding the container, any unpersisted data will be lost. Do you wish to continue?', 'The upgrade operation requires rebuilding the container, any unpersisted data will be lost. Do you wish to continue?',
oldImage: 'Current image', oldImage: 'Current image',
sameImageContainer: 'Same-image containers',
sameImageHelper: 'Containers using the same image can be batch upgraded after selection',
targetImage: 'Target image', targetImage: 'Target image',
imageLoadErr: 'No image name detected for the container', imageLoadErr: 'No image name detected for the container',
appHelper: 'The container comes from the app store, and upgrading may make the service unavailable.', appHelper: 'The container comes from the app store, and upgrading may make the service unavailable.',

View file

@ -783,6 +783,8 @@ const message = {
upgradeWarning2: 'アップグレード操作ではコンテナを再構築する必要があります続けたいですか', upgradeWarning2: 'アップグレード操作ではコンテナを再構築する必要があります続けたいですか',
oldImage: '現在の画像', oldImage: '現在の画像',
targetImage: 'ターゲット画像', targetImage: 'ターゲット画像',
sameImageContainer: '同一イメージコンテナ',
sameImageHelper: '同一イメージを使用するコンテナは選択後一括アップグレード可能',
imageLoadErr: 'コンテナの画像名は検出されません', imageLoadErr: 'コンテナの画像名は検出されません',
appHelper: appHelper:
'このコンテナはアプリストアから取得されたものでありアップグレードによってサービスが利用不可になる可能性があります', 'このコンテナはアプリストアから取得されたものでありアップグレードによってサービスが利用不可になる可能性があります',

View file

@ -777,6 +777,8 @@ const message = {
upgradeWarning2: upgradeWarning2:
'업그레이드 작업은 컨테이너를 재빌드해야 하며, 비지속적인 데이터가 손실됩니다. 계속하시겠습니까?', '업그레이드 작업은 컨테이너를 재빌드해야 하며, 비지속적인 데이터가 손실됩니다. 계속하시겠습니까?',
oldImage: '현재 이미지', oldImage: '현재 이미지',
sameImageContainer: '동일 이미지 컨테이너',
sameImageHelper: '동일한 이미지를 사용하는 컨테이너는 선택 일괄 업그레이드 가능',
targetImage: '대상 이미지', targetImage: '대상 이미지',
imageLoadErr: '컨테이너에 대한 이미지 이름이 감지되지 않았습니다.', imageLoadErr: '컨테이너에 대한 이미지 이름이 감지되지 않았습니다.',
appHelper: ' 컨테이너는 스토어에서 왔으며 업그레이드 서비스가 중단될 있습니다.', appHelper: ' 컨테이너는 스토어에서 왔으며 업그레이드 서비스가 중단될 있습니다.',

View file

@ -798,6 +798,8 @@ const message = {
upgradeWarning2: upgradeWarning2:
'Operasi peningkatan memerlukan pembinaan semula kontena, sebarang data yang tidak disimpan akan hilang. Adakah anda mahu meneruskan?', 'Operasi peningkatan memerlukan pembinaan semula kontena, sebarang data yang tidak disimpan akan hilang. Adakah anda mahu meneruskan?',
oldImage: 'Imej semasa', oldImage: 'Imej semasa',
sameImageContainer: 'Kontena imej sama',
sameImageHelper: 'Kontena yang menggunakan imej sama boleh dinaik taraf secara berkumpulan setelah dipilih',
targetImage: 'Imej sasaran', targetImage: 'Imej sasaran',
imageLoadErr: 'Tiada nama imej dikesan untuk kontena', imageLoadErr: 'Tiada nama imej dikesan untuk kontena',
appHelper: appHelper:

View file

@ -794,6 +794,8 @@ const message = {
upgradeWarning2: upgradeWarning2:
'A operação de upgrade requer a reconstrução do contêiner, e qualquer dado não persistente será perdido. Deseja continuar?', 'A operação de upgrade requer a reconstrução do contêiner, e qualquer dado não persistente será perdido. Deseja continuar?',
oldImage: 'Imagem atual', oldImage: 'Imagem atual',
sameImageContainer: 'Contêineres com mesma imagem',
sameImageHelper: 'Contêineres usando a mesma imagem podem ser atualizados em lote após seleção',
targetImage: 'Imagem alvo', targetImage: 'Imagem alvo',
imageLoadErr: 'Nenhum nome de imagem detectado para o contêiner', imageLoadErr: 'Nenhum nome de imagem detectado para o contêiner',
appHelper: 'O contêiner vem da loja de aplicativos, e o upgrade pode tornar o serviço indisponível.', appHelper: 'O contêiner vem da loja de aplicativos, e o upgrade pode tornar o serviço indisponível.',

View file

@ -793,6 +793,8 @@ const message = {
upgradeWarning2: upgradeWarning2:
'Операция обновления требует пересборки контейнера, все несохраненные данные будут потеряны. Хотите продолжить?', 'Операция обновления требует пересборки контейнера, все несохраненные данные будут потеряны. Хотите продолжить?',
oldImage: 'Текущий образ', oldImage: 'Текущий образ',
sameImageContainer: 'Контейнеры с одинаковым образом',
sameImageHelper: 'Контейнеры, использующие один образ, можно массово обновить после выбора',
targetImage: 'Целевой образ', targetImage: 'Целевой образ',
imageLoadErr: 'Не обнаружено имя образа для контейнера', imageLoadErr: 'Не обнаружено имя образа для контейнера',
appHelper: 'Контейнер происходит из магазина приложений, и обновление может сделать сервис недоступным.', appHelper: 'Контейнер происходит из магазина приложений, и обновление может сделать сервис недоступным.',

View file

@ -815,6 +815,8 @@ const message = {
upgradeWarning2: upgradeWarning2:
'Yükseltme işlemi konteynerin yeniden oluşturulmasını gerektirir, kalıcı olmayan tüm veriler kaybedilecektir. Devam etmek istiyor musunuz?', 'Yükseltme işlemi konteynerin yeniden oluşturulmasını gerektirir, kalıcı olmayan tüm veriler kaybedilecektir. Devam etmek istiyor musunuz?',
oldImage: 'Mevcut imaj', oldImage: 'Mevcut imaj',
sameImageContainer: 'Aynı imajlı konteynerler',
sameImageHelper: 'Aynı imajı kullanan konteynerlar seçilerek toplu şekilde güncellenebilir',
targetImage: 'Hedef imaj', targetImage: 'Hedef imaj',
imageLoadErr: 'Konteyner için imaj adı algılanmadı', imageLoadErr: 'Konteyner için imaj adı algılanmadı',
appHelper: 'Konteyner uygulama mağazasından geliyor ve yükseltme hizmeti kullanılamaz hale getirebilir.', appHelper: 'Konteyner uygulama mağazasından geliyor ve yükseltme hizmeti kullanılamaz hale getirebilir.',

View file

@ -770,6 +770,8 @@ const message = {
upgradeHelper: '倉庫名稱/鏡像名稱:鏡像版本', upgradeHelper: '倉庫名稱/鏡像名稱:鏡像版本',
upgradeWarning2: '升級操作需要重建容器任何未持久化的數據將會丟失是否繼續', upgradeWarning2: '升級操作需要重建容器任何未持久化的數據將會丟失是否繼續',
oldImage: '當前鏡像', oldImage: '當前鏡像',
sameImageContainer: '同鏡像容器',
sameImageHelper: '同鏡像容器可勾選後批量升級',
targetImage: '目標鏡像', targetImage: '目標鏡像',
imageLoadErr: '未檢測到容器的鏡像名稱', imageLoadErr: '未檢測到容器的鏡像名稱',
appHelper: '該容器來源於應用商店升級可能導致該服務不可用', appHelper: '該容器來源於應用商店升級可能導致該服務不可用',

View file

@ -769,6 +769,8 @@ const message = {
upgradeHelper: '仓库名称/镜像名称:镜像版本', upgradeHelper: '仓库名称/镜像名称:镜像版本',
upgradeWarning2: '升级操作需要重建容器任何未持久化的数据将会丢失是否继续', upgradeWarning2: '升级操作需要重建容器任何未持久化的数据将会丢失是否继续',
oldImage: '当前镜像', oldImage: '当前镜像',
sameImageContainer: '同镜像容器',
sameImageHelper: '同镜像容器可勾选后批量升级',
targetImage: '目标镜像', targetImage: '目标镜像',
imageLoadErr: '未检测到容器的镜像名称', imageLoadErr: '未检测到容器的镜像名称',
appHelper: '该容器来源于应用商店升级可能导致该服务不可用', appHelper: '该容器来源于应用商店升级可能导致该服务不可用',

View file

@ -1,11 +1,5 @@
<template> <template>
<DrawerPro <DrawerPro v-model="drawerVisible" :header="$t('commons.button.upgrade')" @close="handleClose" size="large">
v-model="drawerVisible"
:header="$t('commons.button.upgrade')"
@close="handleClose"
:resource="form.name"
size="large"
>
<el-alert <el-alert
:title="$t('container.appHelper')" :title="$t('container.appHelper')"
v-if="form.fromApp" v-if="form.fromApp"
@ -20,6 +14,24 @@
</el-tooltip> </el-tooltip>
<el-tag v-else>{{ form.oldImageName }}</el-tag> <el-tag v-else>{{ form.oldImageName }}</el-tag>
</el-form-item> </el-form-item>
<el-form-item :label="$t('container.sameImageContainer')" v-if="containerOptions.length > 1">
<div class="w-full">
<el-checkbox v-model="checkAll" :indeterminate="isIndeterminate" @change="handleCheckAllChange">
{{ $t('commons.table.all') }}
</el-checkbox>
</div>
<el-checkbox-group v-model="form.names" @change="handleCheckedChange">
<el-checkbox
v-for="item in containerOptions"
:key="item.name"
:label="item.name"
:value="item.name"
>
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
<span class="input-help">{{ $t('container.sameImageHelper') }}</span>
</el-form-item>
<el-form-item prop="newImageName" :rules="Rules.imageName"> <el-form-item prop="newImageName" :rules="Rules.imageName">
<template #label> <template #label>
{{ $t('container.targetImage') }} {{ $t('container.targetImage') }}
@ -52,20 +64,20 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { upgradeContainer } from '@/api/modules/container'; import { listContainerByImage, upgradeContainer } from '@/api/modules/container';
import { Rules } from '@/global/form-rules'; import { Rules } from '@/global/form-rules';
import TaskLog from '@/components/log/task/index.vue'; import TaskLog from '@/components/log/task/index.vue';
import i18n from '@/lang'; import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { newUUID } from '@/utils/util'; import { newUUID } from '@/utils/util';
import { ElForm } from 'element-plus'; import { CheckboxValueType, ElForm } from 'element-plus';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
const loading = ref(false); const loading = ref(false);
const taskLogRef = ref(); const taskLogRef = ref();
const form = reactive({ const form = reactive({
name: '', names: [],
oldImageName: '', oldImageName: '',
newImageName: '', newImageName: '',
hasName: true, hasName: true,
@ -79,13 +91,18 @@ const formRef = ref<FormInstance>();
const drawerVisible = ref<boolean>(false); const drawerVisible = ref<boolean>(false);
type FormInstance = InstanceType<typeof ElForm>; type FormInstance = InstanceType<typeof ElForm>;
const containerOptions = ref([]);
const isIndeterminate = ref();
const checkAll = ref();
interface DialogProps { interface DialogProps {
container: string; container: string;
image: string; image: string;
fromApp: boolean; fromApp: boolean;
} }
const acceptParams = (props: DialogProps): void => { const acceptParams = (props: DialogProps): void => {
form.name = props.container; form.names = [props.container];
isIndeterminate.value = true;
form.oldImageName = props.image; form.oldImageName = props.image;
form.fromApp = props.fromApp; form.fromApp = props.fromApp;
form.hasName = props.image.indexOf('sha256:') === -1; form.hasName = props.image.indexOf('sha256:') === -1;
@ -94,10 +111,32 @@ const acceptParams = (props: DialogProps): void => {
} else { } else {
form.newImageName = ''; form.newImageName = '';
} }
loadContainers();
drawerVisible.value = true; drawerVisible.value = true;
}; };
const emit = defineEmits<{ (e: 'search'): void }>(); const emit = defineEmits<{ (e: 'search'): void }>();
const loadContainers = async () => {
const res = await listContainerByImage(form.oldImageName);
containerOptions.value = res.data || [];
};
const handleCheckAllChange = (val: CheckboxValueType) => {
form.names = [];
if (!val) {
isIndeterminate.value = false;
return;
}
for (const item of containerOptions.value) {
form.names.push(item.name);
}
};
const handleCheckedChange = (value: CheckboxValueType[]) => {
const checkedCount = value.length;
checkAll.value = checkedCount === containerOptions.value.length;
isIndeterminate.value = checkedCount > 0 && checkedCount < containerOptions.value.length;
};
const onSubmit = async (formEl: FormInstance | undefined) => { const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;
formEl.validate(async (valid) => { formEl.validate(async (valid) => {
@ -109,7 +148,7 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
let taskID = newUUID(); let taskID = newUUID();
let param = { let param = {
taskID: taskID, taskID: taskID,
name: form.name, names: form.names,
image: form.newImageName, image: form.newImageName,
forcePull: form.forcePull, forcePull: form.forcePull,
}; };