diff --git a/agent/app/service/container.go b/agent/app/service/container.go index 0d823f411..0c28ebe87 100644 --- a/agent/app/service/container.go +++ b/agent/app/service/container.go @@ -1660,6 +1660,10 @@ func calculateNetwork(network map[string]container.NetworkStats) (float64, float } func checkImageExist(client *client.Client, imageItem string) bool { + return checkImageExistContext(context.Background(), client, imageItem) +} + +func checkImageExistContext(ctx context.Context, client *client.Client, imageItem string) bool { if client == nil { var err error client, err = docker.NewDockerClient() @@ -1667,7 +1671,7 @@ func checkImageExist(client *client.Client, imageItem string) bool { return false } } - images, err := client.ImageList(context.Background(), image.ListOptions{}) + images, err := client.ImageList(ctx, image.ListOptions{}) if err != nil { return false } @@ -1709,6 +1713,10 @@ func checkImageLike(client *client.Client, imageName string) bool { } func pullImages(task *task.Task, client *client.Client, imageName string) error { + return pullImagesContext(context.Background(), task, client, imageName) +} + +func pullImagesContext(ctx context.Context, task *task.Task, client *client.Client, imageName string) error { dockerCli := docker.NewClientWithExist(client) repos, err := imageRepoRepo.List() if err != nil { @@ -1716,7 +1724,7 @@ func pullImages(task *task.Task, client *client.Client, imageName string) error } imageRepo := selectImageRepo(imageName, repos) if imageRepo == nil || !imageRepo.Auth { - return dockerCli.PullImageWithProcess(task, imageName) + return dockerCli.PullImageWithProcessContext(ctx, task, imageName) } options := image.PullOptions{} @@ -1729,7 +1737,7 @@ func pullImages(task *task.Task, client *client.Client, imageName string) error return err } options.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON) - return dockerCli.PullImageWithProcessAndOptions(task, imageName, options) + return dockerCli.PullImageWithProcessAndOptionsContext(ctx, task, imageName, options) } func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo { diff --git a/agent/app/service/container_update.go b/agent/app/service/container_update.go index 8e6e4e4a4..99c4f9325 100644 --- a/agent/app/service/container_update.go +++ b/agent/app/service/container_update.go @@ -91,81 +91,95 @@ func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error { return nil } +const containerUpgradeConcurrency = 2 + +// Shared by all upgrade requests handled by this agent. +var containerUpgradeSlots = make(chan struct{}, containerUpgradeConcurrency) + func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error { client, err := docker.NewDockerClient() if err != nil { return err } - ctx := context.Background() taskItem, err := task.NewTaskWithOps(req.Image, task.TaskUpgrade, task.TaskScopeImage, req.TaskID, 1) if err != nil { _ = client.Close() global.LOG.Errorf("new task for create container failed, err: %v", err) return err } - go func() { - defer client.Close() - 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 err := pullImages(taskItem, client, req.Image); err != nil { - if !req.ForcePull { - return err - } - return fmt.Errorf("pull image %s failed, err: %v", req.Image, err) - } + taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error { + ctx, cancel := context.WithTimeout(context.Background(), containerUpgradePullTimeout) + defer cancel() + taskItem.LogStart(i18n.GetWithName("ContainerImagePull", req.Image)) + if req.ForcePull || !checkImageExistContext(ctx, client, req.Image) { + if err := pullImagesContext(ctx, taskItem, client, req.Image); err != nil { + return fmt.Errorf("pull image %s failed: %w", req.Image, err) + } + } + return nil + }, nil) + var upgradeErrors []error + for _, item := range req.Names { + item := item + taskItem.AddSubTaskWithIgnoreErr(i18n.GetWithName("ContainerUpgradeItem", item), func(t *task.Task) error { + t.Logf("----------------- %s -----------------", item) + t.LogStart(i18n.GetWithName("ContainerAcquireLock", item)) + ctx, cancel := context.WithTimeout(context.Background(), containerUpgradeReplaceTimeout) + defer cancel() + unlock, lockErr := containerOperationLock.lockContext(ctx, item) + if lockErr != nil { + upgradeErrors = append(upgradeErrors, fmt.Errorf("wait for container %s lock: %w", item, lockErr)) + return lockErr + } + defer unlock() + t.LogWithStatus(i18n.GetWithName("ContainerAcquireLock", item), nil) + + oldContainer, inspectErr := client.ContainerInspect(ctx, item) + t.LogWithStatus(i18n.GetWithName("ContainerLoadInfo", item), inspectErr) + if inspectErr != nil { + err := fmt.Errorf("reload container %s failed: %w", item, inspectErr) + upgradeErrors = append(upgradeErrors, err) + return err + } + config := cloneContainerConfig(oldContainer.Config) + config.Image = req.Image + if config.Labels == nil { + config.Labels = make(map[string]string) + } + config.Labels[containerUpgradeOwnerLabel] = oldContainer.ID + hostConf := cloneContainerHostConfig(oldContainer.HostConfig) + preserveContainerVolumeMounts(hostConf, oldContainer.Mounts) + networkConf, extraNetworks := buildContainerRecoverNetworkConfig(oldContainer.NetworkSettings, hostConf) + if err := normalizeContainerEndpointSettings(ctx, client, networkConf, extraNetworks); err != nil { + upgradeErr := fmt.Errorf("prepare networks for container %s failed: %w", item, err) + upgradeErrors = append(upgradeErrors, upgradeErr) + return upgradeErr + } + cleanupErr, err := switchContainerWithRecovery(ctx, client, item, oldContainer, func() (container.CreateResponse, error) { + return createContainerWithNetworks(ctx, client, config, hostConf, networkConf, extraNetworks, item) + }, config.Tty, t, true) + if err != nil { + upgradeErr := fmt.Errorf("upgrade container %s failed: %w", item, err) + upgradeErrors = append(upgradeErrors, upgradeErr) + return upgradeErr + } + if cleanupErr != nil { + t.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr)) } return nil - }, nil) - var upgradeErrors []error - for _, item := range req.Names { - item := item - taskItem.AddSubTaskWithIgnoreErr(i18n.GetWithName("ContainerUpgradeItem", item), func(t *task.Task) error { - t.Logf("----------------- %s -----------------", item) - t.LogStart(i18n.GetWithName("ContainerAcquireLock", item)) - unlock := containerOperationLock.lock(item) - defer unlock() - t.LogWithStatus(i18n.GetWithName("ContainerAcquireLock", item), nil) - - oldContainer, inspectErr := client.ContainerInspect(ctx, item) - t.LogWithStatus(i18n.GetWithName("ContainerLoadInfo", item), inspectErr) - if inspectErr != nil { - err := fmt.Errorf("reload container %s failed: %w", item, inspectErr) - upgradeErrors = append(upgradeErrors, err) - return err - } - config := cloneContainerConfig(oldContainer.Config) - config.Image = req.Image - hostConf := cloneContainerHostConfig(oldContainer.HostConfig) - preserveContainerVolumeMounts(hostConf, oldContainer.Mounts) - networkConf, extraNetworks := buildContainerRecoverNetworkConfig(oldContainer.NetworkSettings, hostConf) - if err := normalizeContainerEndpointSettings(ctx, client, networkConf, extraNetworks); err != nil { - upgradeErr := fmt.Errorf("prepare networks for container %s failed: %w", item, err) - upgradeErrors = append(upgradeErrors, upgradeErr) - return upgradeErr - } - cleanupErr, err := switchContainer(ctx, client, item, oldContainer, func() (container.CreateResponse, error) { - return createContainerWithNetworks(ctx, client, config, hostConf, networkConf, extraNetworks, item) - }, config.Tty, t) - if err != nil { - upgradeErr := fmt.Errorf("upgrade container %s failed: %w", item, err) - upgradeErrors = append(upgradeErrors, upgradeErr) - return upgradeErr - } - if cleanupErr != nil { - t.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr)) - } - return nil - }) - } - taskItem.AddSubTask(i18n.GetMsgByKey("ContainerUpgradeSummary"), func(t *task.Task) error { - return errors.Join(upgradeErrors...) - }, nil) - if err := taskItem.Execute(); err != nil { - global.LOG.Error(err.Error()) - } - }() - + }) + } + taskItem.AddSubTask(i18n.GetMsgByKey("ContainerUpgradeSummary"), func(t *task.Task) error { + return errors.Join(upgradeErrors...) + }, nil) + // Stage contexts own the deadlines, including a separate recovery budget. + for _, subTask := range taskItem.SubTasks { + subTask.Timeout = 0 + } + if err := taskItem.ExecuteQueued(containerUpgradeSlots, "ContainerUpgradeWaiting", func() { _ = client.Close() }); err != nil { + _ = client.Close() + return err + } return nil } @@ -192,11 +206,16 @@ type containerOperationMutex struct { var containerOperationLock containerOperationMutex type containerOperationLockEntry struct { - mutex sync.Mutex + token chan struct{} references int } func (l *containerOperationMutex) lock(names ...string) func() { + unlock, _ := l.lockContext(context.Background(), names...) + return unlock +} + +func (l *containerOperationMutex) lockContext(ctx context.Context, names ...string) (func(), error) { nameSet := make(map[string]struct{}, len(names)) for _, name := range names { if name != "" { @@ -209,7 +228,7 @@ func (l *containerOperationMutex) lock(names ...string) func() { } sort.Strings(orderedNames) if len(orderedNames) == 0 { - return func() {} + return func() {}, nil } l.mutex.Lock() @@ -220,7 +239,7 @@ func (l *containerOperationMutex) lock(names ...string) func() { for _, name := range orderedNames { entry := l.locks[name] if entry == nil { - entry = &containerOperationLockEntry{} + entry = &containerOperationLockEntry{token: make(chan struct{}, 1)} l.locks[name] = entry } entry.references++ @@ -228,12 +247,10 @@ func (l *containerOperationMutex) lock(names ...string) func() { } l.mutex.Unlock() - for _, entry := range entries { - entry.mutex.Lock() - } - return func() { - for index := len(entries) - 1; index >= 0; index-- { - entries[index].mutex.Unlock() + acquired := 0 + release := func() { + for index := acquired - 1; index >= 0; index-- { + <-entries[index].token } l.mutex.Lock() defer l.mutex.Unlock() @@ -245,6 +262,20 @@ func (l *containerOperationMutex) lock(names ...string) func() { } } } + for _, entry := range entries { + select { + case entry.token <- struct{}{}: + acquired++ + case <-ctx.Done(): + release() + return nil, ctx.Err() + } + } + if err := ctx.Err(); err != nil { + release() + return nil, err + } + return release, nil } type containerNetworkAttachment struct { @@ -272,6 +303,131 @@ func switchContainer( createNew func() (container.CreateResponse, error), tty bool, logger containerSwitchLogger, +) (cleanupErr error, err error) { + return switchContainerWithRecovery(ctx, cli, name, oldContainer, createNew, tty, logger, false) +} + + +const ( + containerUpgradePullTimeout = 30 * time.Minute + containerUpgradeReplaceTimeout = 15 * time.Minute + containerUpgradeRecoveryTimeout = 5 * time.Minute + containerUpgradeOwnerLabel = "io.1panel.upgrade.original-id" +) + +// Reconcile against Docker's actual state: a failed request may already have +// changed names, created a container, or disconnected a network on the daemon. +func recoverContainerUpgrade(ctx context.Context, cli containerSwitchClient, name string, old container.InspectResponse, newID string, logger containerSwitchLogger) error { + original, err := cli.ContainerInspect(ctx, old.ID) + if err != nil { + return fmt.Errorf("inspect original %s: %w", old.ID, err) + } + if original.State == nil { + return fmt.Errorf("original container state unavailable") + } + + replacement, inspectErr := cli.ContainerInspect(ctx, name) + if inspectErr != nil && !client.IsErrNotFound(inspectErr) { + return fmt.Errorf("inspect replacement by name: %w", inspectErr) + } + if inspectErr == nil && replacement.ID != old.ID { + if replacement.Config == nil || replacement.Config.Labels[containerUpgradeOwnerLabel] != old.ID { + return fmt.Errorf("container %s belongs to another operation; refusing to remove it", name) + } + if newID != "" && replacement.ID != newID { + return fmt.Errorf("replacement ID changed; refusing to remove it") + } + newID = replacement.ID + } + if newID != "" { + replacement, inspectErr = cli.ContainerInspect(ctx, newID) + if inspectErr != nil && !client.IsErrNotFound(inspectErr) { + return fmt.Errorf("inspect replacement %s: %w", newID, inspectErr) + } + if inspectErr == nil { + if replacement.ID == old.ID || replacement.Config == nil || replacement.Config.Labels[containerUpgradeOwnerLabel] != old.ID { + return fmt.Errorf("replacement identity could not be confirmed") + } + err = cli.ContainerRemove(ctx, newID, container.RemoveOptions{Force: true, RemoveVolumes: false}) + logContainerSwitchStep(logger, "ContainerRollbackRemoveReplacement", name, err) + if err != nil && !client.IsErrNotFound(err) { + return fmt.Errorf("remove replacement: %w", err) + } + } + } + if strings.TrimPrefix(original.Name, "/") != name { + err = cli.ContainerRename(ctx, old.ID, name) + logContainerSwitchStep(logger, "ContainerRollbackRenameOld", name, err) + if err != nil { + return fmt.Errorf("restore original name: %w", err) + } + } + // Reload after mutations; connect only networks which are actually missing. + original, err = cli.ContainerInspect(ctx, old.ID) + if err != nil { + return err + } + primary, extras := buildContainerRecoverNetworkConfig(old.NetworkSettings, old.HostConfig) + wanted := make(map[string]*network.EndpointSettings) + if primary != nil { + for n, endpoint := range primary.EndpointsConfig { + if n != "bridge" { + wanted[n] = endpoint + } + } + } + for n, endpoint := range extras { + if n != "bridge" { + wanted[n] = endpoint + } + } + var reconnectErr error + for n, endpoint := range wanted { + if original.NetworkSettings != nil && original.NetworkSettings.Networks[n] != nil { + continue + } + err = cli.NetworkConnect(ctx, n, old.ID, endpoint) + logContainerSwitchStep(logger, "ContainerRollbackReconnectOld", n, err) + if err != nil { + reconnectErr = errors.Join(reconnectErr, fmt.Errorf("reconnect network %s: %w", n, err)) + } + } + if reconnectErr != nil { + return reconnectErr + } + if old.State != nil && old.State.Running { + if original.State == nil { + return fmt.Errorf("original state unavailable after recovery") + } + if !original.State.Running { + err = restartOriginalContainer(ctx, cli, old.ID) + logContainerSwitchStep(logger, "ContainerRollbackRestartOld", name, err) + if err != nil { + return err + } + } + } + verified, err := cli.ContainerInspect(ctx, old.ID) + if err != nil { + return fmt.Errorf("verify recovery: %w", err) + } + if strings.TrimPrefix(verified.Name, "/") != name { + return fmt.Errorf("original name was not restored") + } + if old.State != nil && old.State.Running && (verified.State == nil || !verified.State.Running) { + return fmt.Errorf("original container is not running after recovery") + } + for n := range wanted { + if verified.NetworkSettings == nil || verified.NetworkSettings.Networks[n] == nil { + return fmt.Errorf("network %s was not restored", n) + } + } + return nil +} + +func switchContainerWithRecovery(ctx context.Context, cli containerSwitchClient, name string, + oldContainer container.InspectResponse, createNew func() (container.CreateResponse, error), + tty bool, logger containerSwitchLogger, boundedRecovery bool, ) (cleanupErr error, err error) { if oldContainer.ID == "" { return nil, fmt.Errorf("original container ID is empty") @@ -281,9 +437,32 @@ func switchContainer( return nil, fmt.Errorf("cannot safely replace container %s with auto-remove enabled", name) } + recoverUpgrade := func(newID string, cause error) error { + recoveryCtx, cancel := context.WithTimeout(context.Background(), containerUpgradeRecoveryTimeout) + defer cancel() + if newID != "" { + logContainerStartupLogs(recoveryCtx, cli, newID, name, tty, logger) + } + recoveryErr := recoverContainerUpgrade(recoveryCtx, cli, name, oldContainer, newID, logger) + if ctx.Err() != nil { + recoveryErr = errors.Join(recoveryErr, fmt.Errorf("Docker request timed out or was canceled; daemon-side completion cannot be confirmed: %w", ctx.Err())) + } + if recoveryErr != nil { + message := fmt.Sprintf("Recovery unconfirmed for container %s (original ID %s, backup %s): %v", name, oldContainer.ID, containerSwitchBackupName(oldContainer.ID), recoveryErr) + if logger != nil { + logger.Log(message) + } + return errors.Join(cause, errors.New(message)) + } + return cause + } + if wasRunning { if err := cli.ContainerStop(ctx, oldContainer.ID, container.StopOptions{}); err != nil { logContainerSwitchStep(logger, "ContainerStopOld", name, err) + if boundedRecovery { + return nil, recoverUpgrade("", fmt.Errorf("stop original container failed: %w", err)) + } current, inspectErr := cli.ContainerInspect(ctx, oldContainer.ID) if inspectErr == nil && current.State != nil && !current.State.Running { restartErr := restartOriginalContainer(ctx, cli, oldContainer.ID) @@ -298,6 +477,9 @@ func switchContainer( backupName := containerSwitchBackupName(oldContainer.ID) if err := cli.ContainerRename(ctx, oldContainer.ID, backupName); err != nil { logContainerSwitchStep(logger, "ContainerRenameOld", name, err) + if boundedRecovery { + return nil, recoverUpgrade("", fmt.Errorf("rename original container failed: %w", err)) + } if wasRunning { restartErr := restartOriginalContainer(ctx, cli, oldContainer.ID) logContainerSwitchStep(logger, "ContainerRollbackRestartOld", name, restartErr) @@ -309,6 +491,9 @@ func switchContainer( disconnectedNetworks, disconnectErr := disconnectOriginalContainerNetworks(ctx, cli, oldContainer) logContainerSwitchStep(logger, "ContainerDisconnectOld", backupName, disconnectErr) if disconnectErr != nil { + if boundedRecovery { + return nil, recoverUpgrade("", disconnectErr) + } rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, "", disconnectedNetworks, logger) return nil, errors.Join(disconnectErr, rollbackErr) } @@ -316,11 +501,17 @@ func switchContainer( created, createErr := createNew() logContainerSwitchStep(logger, "ContainerCreateReplacement", name, createErr) if createErr != nil { + if boundedRecovery { + return nil, recoverUpgrade(created.ID, createErr) + } rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger) return nil, errors.Join(createErr, rollbackErr) } if err := cli.ContainerStart(ctx, created.ID, container.StartOptions{}); err != nil { logContainerSwitchStep(logger, "ContainerStartReplacement", name, err) + if boundedRecovery { + return nil, recoverUpgrade(created.ID, fmt.Errorf("start new container failed: %w", err)) + } logContainerStartupLogs(ctx, cli, created.ID, name, tty, logger) rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger) return nil, errors.Join(fmt.Errorf("start new container failed: %w", err), rollbackErr) @@ -329,6 +520,9 @@ func switchContainer( if wasRunning { if err := waitContainerReady(ctx, cli, created.ID); err != nil { logContainerSwitchStep(logger, "ContainerWaitReplacement", name, err) + if boundedRecovery { + return nil, recoverUpgrade(created.ID, fmt.Errorf("new container readiness check failed: %w", err)) + } logContainerStartupLogs(ctx, cli, created.ID, name, tty, logger) rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger) return nil, errors.Join(fmt.Errorf("new container readiness check failed: %w", err), rollbackErr) @@ -336,6 +530,11 @@ func switchContainer( logContainerSwitchStep(logger, "ContainerWaitReplacement", name, nil) } + if boundedRecovery { + cleanupCtx, cancel := context.WithTimeout(context.Background(), containerUpgradeRecoveryTimeout) + defer cancel() + ctx = cleanupCtx + } cleanupErr = cli.ContainerRemove(ctx, oldContainer.ID, container.RemoveOptions{Force: true, RemoveVolumes: false}) logContainerSwitchStep(logger, "ContainerRemoveOld", backupName, cleanupErr) return cleanupErr, nil diff --git a/agent/app/task/task.go b/agent/app/task/task.go index 3883f1350..ea6212120 100644 --- a/agent/app/task/task.go +++ b/agent/app/task/task.go @@ -292,10 +292,36 @@ func (t *Task) updateTask(task *model.Task) { _ = t.taskRepo.Update(context.Background(), task) } +// ExecuteQueued registers the task before returning and limits concurrent execution. +// Subtasks must use Timeout = 0 and manage their own operation and recovery deadlines +// so that a slot is held until the action and its cleanup have actually returned. +func (t *Task) ExecuteQueued(slots chan struct{}, waitingStep string, onFinish func()) error { + t.Task.CurrentStep = waitingStep + if err := t.taskRepo.Save(context.Background(), t.Task); err != nil { + _ = t.logFile.Close() + global.RemoveTaskCancel(t.TaskID) + return err + } + t.Log(i18n.GetMsgByKey(waitingStep)) + go func() { + slots <- struct{}{} + defer func() { <-slots }() + defer onFinish() + if err := t.executeRegistered(); err != nil { + global.LOG.Error(err.Error()) + } + }() + return nil +} + func (t *Task) Execute() error { if err := t.taskRepo.Save(context.Background(), t.Task); err != nil { return err } + return t.executeRegistered() +} + +func (t *Task) executeRegistered() error { var err error t.Log(i18n.GetWithName("TaskStart", t.Name)) for i := 0; i < len(t.SubTasks); i++ { diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index 4e29cdfc5..c61e82476 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -800,3 +800,5 @@ ErrFeishuUpgradeBackup: "A full application backup is required for the Feishu co ErrFeishuUpgradeMounts: "These mounts cannot be fully backed up or restored. Use standard data mounts inside the application directory." AgentFeishuRecoveryDisabled: "Gateway recovered with Feishu disabled; the original configuration backup is retained." + +ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index 05e44451d..e64a97c1b 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -800,3 +800,5 @@ ErrFeishuUpgradeBackup: "La actualización de compatibilidad de Feishu requiere ErrFeishuUpgradeMounts: "Estos montajes no permiten una copia y restauración completas. Use los montajes de datos estándar dentro del directorio de la aplicación." AgentFeishuRecoveryDisabled: "Puerta de enlace recuperada con Feishu desactivado; se conserva la copia de configuración original." + +ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" diff --git a/agent/i18n/lang/fa.yaml b/agent/i18n/lang/fa.yaml index 8011e3b16..d6716865e 100644 --- a/agent/i18n/lang/fa.yaml +++ b/agent/i18n/lang/fa.yaml @@ -801,3 +801,5 @@ ErrFeishuUpgradeBackup: "ارتقای سازگاری Feishu به پشتیبان ErrFeishuUpgradeMounts: "این اتصال‌های ذخیره‌سازی کاملاً قابل پشتیبان‌گیری و بازیابی نیستند. از مسیرهای استاندارد داده در پوشه برنامه استفاده کنید." AgentFeishuRecoveryDisabled: "دروازه بازیابی شد و Feishu غیرفعال است؛ پشتیبان تنظیمات اصلی حفظ شد." + +ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index 127078b53..451dee002 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -800,3 +800,5 @@ ErrFeishuUpgradeBackup: "Feishu 互換更新にはアプリ全体のバックア ErrFeishuUpgradeMounts: "現在のマウントは完全なバックアップ・復元に対応しません。アプリディレクトリ内の標準データマウントを使用してください。" AgentFeishuRecoveryDisabled: "ゲートウェイを復旧しました。Feishu は無効のままで、元の設定のバックアップは保持しています。" + +ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 68706ecaa..a29e79dcc 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -800,3 +800,5 @@ ErrFeishuUpgradeBackup: "Feishu 호환 업그레이드에는 전체 앱 백업 ErrFeishuUpgradeMounts: "현재 마운트는 전체 백업 및 복원을 지원하지 않습니다. 앱 디렉터리 내부의 표준 데이터 마운트를 사용하세요." AgentFeishuRecoveryDisabled: "게이트웨이가 복구되었습니다. Feishu는 비활성 상태이며 원본 설정 백업은 유지됩니다." + +ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" diff --git a/agent/i18n/lang/lo.yaml b/agent/i18n/lang/lo.yaml index 8c8cdf978..1b97892a7 100644 --- a/agent/i18n/lang/lo.yaml +++ b/agent/i18n/lang/lo.yaml @@ -791,3 +791,5 @@ ErrFeishuUpgradeBackup: "ການອັບເກຣດ Feishu ຕ້ອງສ ErrFeishuUpgradeMounts: "ຈຸດເຊື່ອມຂໍ້ມູນນີ້ບໍ່ສາມາດສຳຮອງແລະກູ້ຄືນທັງໝົດໄດ້. ໃຊ້ຈຸດເຊື່ອມມາດຕະຖານພາຍໃນໂຟນເດີແອັບ." AgentFeishuRecoveryDisabled: "ເກດເວກູ້ຄືນແລ້ວ ແຕ່ Feishu ຍັງປິດ; ສຳຮອງການຕັ້ງຄ່າເດີມຍັງຖືກເກັບໄວ້." + +ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index ebb057ab1..5536c3089 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -800,3 +800,5 @@ ErrFeishuUpgradeBackup: "Naik taraf keserasian Feishu memerlukan sandaran aplika ErrFeishuUpgradeMounts: "Lekapan ini tidak boleh disandar dan dipulihkan sepenuhnya. Gunakan lekapan data standard dalam direktori aplikasi." AgentFeishuRecoveryDisabled: "Gerbang dipulihkan dengan Feishu dinyahaktifkan; sandaran konfigurasi asal dikekalkan." + +ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 680c79373..78980c76b 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -800,3 +800,5 @@ ErrFeishuUpgradeBackup: "A atualização de compatibilidade do Feishu exige back ErrFeishuUpgradeMounts: "Estas montagens não permitem backup e restauração completos. Use montagens de dados padrão dentro do diretório do aplicativo." AgentFeishuRecoveryDisabled: "Gateway recuperado com Feishu desativado; o backup da configuração original foi mantido." + +ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index 9dd2c58ed..fe41f9f58 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -800,3 +800,5 @@ ErrFeishuUpgradeBackup: "Для обновления совместимости ErrFeishuUpgradeMounts: "Эти монтирования не поддерживают полное резервное копирование и восстановление. Используйте стандартные данные внутри каталога приложения." AgentFeishuRecoveryDisabled: "Шлюз восстановлен, Feishu отключён; исходная резервная копия конфигурации сохранена." + +ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index fd16d7cda..b9122edfa 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -800,3 +800,5 @@ ErrFeishuUpgradeBackup: "Feishu uyumluluk yükseltmesi tam uygulama yedeği gere ErrFeishuUpgradeMounts: "Bu bağlamalar tam yedeklenip geri yüklenemiyor. Uygulama dizinindeki standart veri bağlamalarını kullanın." AgentFeishuRecoveryDisabled: "Ağ geçidi kurtarıldı, Feishu kapalı; özgün yapılandırma yedeği korundu." + +ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 8fdb70743..959eb0b3d 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -800,3 +800,5 @@ ErrFeishuUpgradeBackup: "飛書相容升級必須開啟完整應用程式備份 ErrFeishuUpgradeMounts: "目前掛載無法完整備份或還原,請使用應用程式目錄內的標準資料掛載。" AgentFeishuRecoveryDisabled: "閘道已恢復,飛書保持停用;原設定備份已保留。" + +ContainerUpgradeWaiting: "等待容器升級任務執行名額" diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 2e1504ea1..c51d41821 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -800,3 +800,5 @@ ErrFeishuUpgradeBackup: "飞书兼容升级必须开启完整应用备份。" ErrFeishuUpgradeMounts: "当前挂载无法完整备份或恢复,请使用应用目录内的标准数据挂载后重试。" AgentFeishuRecoveryDisabled: "网关已恢复,飞书保持停用;原配置备份已保留。" + +ContainerUpgradeWaiting: "等待容器升级任务执行名额" diff --git a/agent/utils/docker/docker.go b/agent/utils/docker/docker.go index c76220396..6c41c5efa 100644 --- a/agent/utils/docker/docker.go +++ b/agent/utils/docker/docker.go @@ -198,12 +198,22 @@ func setLog(id, newLastLine string, task *task.Task) error { } func (c Client) PullImageWithProcessAndOptions(task *task.Task, imageName string, options image.PullOptions) error { - out, err := c.cli.ImagePull(context.Background(), imageName, options) + return c.PullImageWithProcessAndOptionsContext(context.Background(), task, imageName, options) +} + +func (c Client) PullImageWithProcessAndOptionsContext(ctx context.Context, task *task.Task, imageName string, options image.PullOptions) error { + out, err := c.cli.ImagePull(ctx, imageName, options) if err != nil { return err } defer out.Close() - return handlePullImageProcess(out, task) + stopClose := context.AfterFunc(ctx, func() { _ = out.Close() }) + defer stopClose() + err = handlePullImageProcess(out, task) + if ctx.Err() != nil { + return ctx.Err() + } + return err } func handlePullImageProcess(out io.Reader, task *task.Task) error { @@ -320,11 +330,15 @@ func (c Client) BuildImageWithProcessAndOptions(task *task.Task, tar io.ReadClos } func (c Client) PullImageWithProcess(task *task.Task, imageName string) error { + return c.PullImageWithProcessContext(context.Background(), task, imageName) +} + +func (c Client) PullImageWithProcessContext(ctx context.Context, task *task.Task, imageName string) error { options := image.PullOptions{} if authStr, ok := RegistryAuthForImage(imageName); ok { options.RegistryAuth = authStr } - return c.PullImageWithProcessAndOptions(task, imageName, options) + return c.PullImageWithProcessAndOptionsContext(ctx, task, imageName, options) } func logProcess(progress map[string]interface{}, task *task.Task) { diff --git a/frontend/src/api/modules/container.ts b/frontend/src/api/modules/container.ts index 7a48f2ab1..09c15a1a3 100644 --- a/frontend/src/api/modules/container.ts +++ b/frontend/src/api/modules/container.ts @@ -14,8 +14,13 @@ export const searchContainer = (params: Container.ContainerSearch, currentNode?: export const listContainer = () => { return http.post>(`/containers/list`, {}); }; -export const listContainerByImage = (image: string) => { - return http.post>(`/containers/list/byimage`, { name: image }); +export const listContainerByImage = (image: string, node?: string) => { + return http.post>( + `/containers/list/byimage`, + { name: image }, + undefined, + node ? { CurrentNode: node } : undefined, + ); }; export const loadContainerUsers = (name: string) => { return http.post>(`/containers/users`, { name: name }); @@ -60,8 +65,8 @@ export const createContainer = (params: Container.ContainerHelper) => { export const updateContainer = (params: Container.ContainerHelper) => { return http.post(`/containers/update`, params, TimeoutEnum.T_10M); }; -export const upgradeContainer = (params: Container.ContainerUpgrade) => { - return http.post(`/containers/upgrade`, params, TimeoutEnum.T_10M); +export const upgradeContainer = (params: Container.ContainerUpgrade, node?: string) => { + return http.post(`/containers/upgrade`, params, TimeoutEnum.T_10M, node ? { CurrentNode: node } : undefined); }; export const commitContainer = (params: Container.ContainerCommit) => { return http.post(`/containers/commit`, params); diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 6e360610a..78bcdd85f 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -2105,6 +2105,17 @@ const message = { missing: 'Missing', }, container: { + batchUpgrade: 'Batch upgrade', + batchUpgradeLog: 'View log', + batchUpgradeCheck: 'Check task', + batchUpgradeRetry: 'Retry failed submissions', + batchUpgradeSubmitting: 'Submitting', + batchUpgradeSubmitFailed: 'Submission failed', + batchUpgradeUnknown: 'Submission unconfirmed; check the task before resubmitting', + batchUpgradeCanceled: 'Canceled', + batchUpgradeWaiting: 'Queued', + batchUpgradeRunning: 'Upgrading', + batchUpgradeSubmitted: 'Upgrade task submitted', createByCommand: 'Create by command', importContainerBackupTip: 'Import container backup file, only .tar.gz is supported', importComposeBackupTip: 'Import compose backup file, only .tar.gz is supported', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index d35437aaa..03f8cde11 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -2124,6 +2124,17 @@ const message = { missing: 'No existe', }, container: { + batchUpgrade: 'Batch upgrade', + batchUpgradeLog: 'View log', + batchUpgradeCheck: 'Check task', + batchUpgradeRetry: 'Retry failed submissions', + batchUpgradeSubmitting: 'Submitting', + batchUpgradeSubmitFailed: 'Submission failed', + batchUpgradeUnknown: 'Submission unconfirmed; check the task before resubmitting', + batchUpgradeCanceled: 'Canceled', + batchUpgradeWaiting: 'Queued', + batchUpgradeRunning: 'Upgrading', + batchUpgradeSubmitted: 'Upgrade task submitted', createByCommand: 'Crear por comando', importContainerBackupTip: 'Importar archivo de copia de seguridad de contenedor, solo se admite .tar.gz', importComposeBackupTip: 'Importar archivo de copia de seguridad de compose, solo se admite .tar.gz', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index c6849f98f..9d3aa762f 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -2082,6 +2082,17 @@ const message = { missing: 'ناموجود', }, container: { + batchUpgrade: 'Batch upgrade', + batchUpgradeLog: 'View log', + batchUpgradeCheck: 'Check task', + batchUpgradeRetry: 'Retry failed submissions', + batchUpgradeSubmitting: 'Submitting', + batchUpgradeSubmitFailed: 'Submission failed', + batchUpgradeUnknown: 'Submission unconfirmed; check the task before resubmitting', + batchUpgradeCanceled: 'Canceled', + batchUpgradeWaiting: 'Queued', + batchUpgradeRunning: 'Upgrading', + batchUpgradeSubmitted: 'Upgrade task submitted', createByCommand: 'ایجاد با دستور', importContainerBackupTip: 'واردات فایل پشتیبان کانتینر، فقط .tar.gz پشتیبانی می‌شود', importComposeBackupTip: 'واردات فایل پشتیبان compose، فقط .tar.gz پشتیبانی می‌شود', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 67e5d2ec7..873b1fba9 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -2089,6 +2089,17 @@ const message = { missing: '存在しない', }, container: { + batchUpgrade: 'Batch upgrade', + batchUpgradeLog: 'View log', + batchUpgradeCheck: 'Check task', + batchUpgradeRetry: 'Retry failed submissions', + batchUpgradeSubmitting: 'Submitting', + batchUpgradeSubmitFailed: 'Submission failed', + batchUpgradeUnknown: 'Submission unconfirmed; check the task before resubmitting', + batchUpgradeCanceled: 'Canceled', + batchUpgradeWaiting: 'Queued', + batchUpgradeRunning: 'Upgrading', + batchUpgradeSubmitted: 'Upgrade task submitted', createByCommand: 'コマンドで作成', importContainerBackupTip: 'コンテナバックアップファイルをインポートします。.tar.gz のみ対応です', importComposeBackupTip: 'Compose バックアップファイルをインポートします。.tar.gz のみ対応です', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 39319aa4b..e52c76a09 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -2064,6 +2064,17 @@ const message = { missing: '없음', }, container: { + batchUpgrade: 'Batch upgrade', + batchUpgradeLog: 'View log', + batchUpgradeCheck: 'Check task', + batchUpgradeRetry: 'Retry failed submissions', + batchUpgradeSubmitting: 'Submitting', + batchUpgradeSubmitFailed: 'Submission failed', + batchUpgradeUnknown: 'Submission unconfirmed; check the task before resubmitting', + batchUpgradeCanceled: 'Canceled', + batchUpgradeWaiting: 'Queued', + batchUpgradeRunning: 'Upgrading', + batchUpgradeSubmitted: 'Upgrade task submitted', createByCommand: '명령으로 생성', importContainerBackupTip: '컨테이너 백업 파일 가져오기, .tar.gz 형식만 지원합니다', importComposeBackupTip: 'Compose 백업 파일 가져오기, .tar.gz 형식만 지원합니다', diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index 84bafee57..6ca54f396 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -2049,6 +2049,17 @@ const message = { missing: 'ບໍ່ມີ', }, container: { + batchUpgrade: 'Batch upgrade', + batchUpgradeLog: 'View log', + batchUpgradeCheck: 'Check task', + batchUpgradeRetry: 'Retry failed submissions', + batchUpgradeSubmitting: 'Submitting', + batchUpgradeSubmitFailed: 'Submission failed', + batchUpgradeUnknown: 'Submission unconfirmed; check the task before resubmitting', + batchUpgradeCanceled: 'Canceled', + batchUpgradeWaiting: 'Queued', + batchUpgradeRunning: 'Upgrading', + batchUpgradeSubmitted: 'Upgrade task submitted', createByCommand: 'ສ້າງດ້ວຍຄຳສັ່ງ', importContainerBackupTip: 'ນຳເຂົ້າໄຟລ໌ສຳຮອງຄອນເທນເນີ, ຮອງຮັບສະເພາະ .tar.gz', importComposeBackupTip: 'ນຳເຂົ້າໄຟລ໌ສຳຮອງ compose, ຮອງຮັບສະເພາະ .tar.gz', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 524086025..e47867bb5 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -2120,6 +2120,17 @@ const message = { missing: 'Tiada', }, container: { + batchUpgrade: 'Batch upgrade', + batchUpgradeLog: 'View log', + batchUpgradeCheck: 'Check task', + batchUpgradeRetry: 'Retry failed submissions', + batchUpgradeSubmitting: 'Submitting', + batchUpgradeSubmitFailed: 'Submission failed', + batchUpgradeUnknown: 'Submission unconfirmed; check the task before resubmitting', + batchUpgradeCanceled: 'Canceled', + batchUpgradeWaiting: 'Queued', + batchUpgradeRunning: 'Upgrading', + batchUpgradeSubmitted: 'Upgrade task submitted', createByCommand: 'Cipta melalui perintah', importContainerBackupTip: 'Import fail sandaran kontena, hanya format .tar.gz disokong', importComposeBackupTip: 'Import fail sandaran compose, hanya format .tar.gz disokong', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index fa13790ef..b1d9a849d 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -2123,6 +2123,17 @@ const message = { missing: 'Não existe', }, container: { + batchUpgrade: 'Batch upgrade', + batchUpgradeLog: 'View log', + batchUpgradeCheck: 'Check task', + batchUpgradeRetry: 'Retry failed submissions', + batchUpgradeSubmitting: 'Submitting', + batchUpgradeSubmitFailed: 'Submission failed', + batchUpgradeUnknown: 'Submission unconfirmed; check the task before resubmitting', + batchUpgradeCanceled: 'Canceled', + batchUpgradeWaiting: 'Queued', + batchUpgradeRunning: 'Upgrading', + batchUpgradeSubmitted: 'Upgrade task submitted', createByCommand: 'Criar por comando', importContainerBackupTip: 'Importar arquivo de backup de contêiner, apenas .tar.gz é suportado', importComposeBackupTip: 'Importar arquivo de backup de compose, apenas .tar.gz é suportado', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index df7fb1162..4bb185fba 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -2116,6 +2116,17 @@ const message = { missing: 'Отсутствует', }, container: { + batchUpgrade: 'Batch upgrade', + batchUpgradeLog: 'View log', + batchUpgradeCheck: 'Check task', + batchUpgradeRetry: 'Retry failed submissions', + batchUpgradeSubmitting: 'Submitting', + batchUpgradeSubmitFailed: 'Submission failed', + batchUpgradeUnknown: 'Submission unconfirmed; check the task before resubmitting', + batchUpgradeCanceled: 'Canceled', + batchUpgradeWaiting: 'Queued', + batchUpgradeRunning: 'Upgrading', + batchUpgradeSubmitted: 'Upgrade task submitted', createByCommand: 'Создать с помощью команды', importContainerBackupTip: 'Импорт файла резервной копии контейнера, поддерживается только .tar.gz', importComposeBackupTip: 'Импорт файла резервной копии compose, поддерживается только .tar.gz', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 4ec58db5e..5b6887d12 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -2121,6 +2121,17 @@ const message = { missing: 'Mevcut değil', }, container: { + batchUpgrade: 'Batch upgrade', + batchUpgradeLog: 'View log', + batchUpgradeCheck: 'Check task', + batchUpgradeRetry: 'Retry failed submissions', + batchUpgradeSubmitting: 'Submitting', + batchUpgradeSubmitFailed: 'Submission failed', + batchUpgradeUnknown: 'Submission unconfirmed; check the task before resubmitting', + batchUpgradeCanceled: 'Canceled', + batchUpgradeWaiting: 'Queued', + batchUpgradeRunning: 'Upgrading', + batchUpgradeSubmitted: 'Upgrade task submitted', createByCommand: 'Komutla oluştur', importContainerBackupTip: 'Konteyner yedek dosyasını içe aktar, yalnızca .tar.gz desteklenir', importComposeBackupTip: 'Compose yedek dosyasını içe aktar, yalnızca .tar.gz desteklenir', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 5dab47b14..3d01212e9 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -1986,6 +1986,17 @@ const message = { missing: '不存在', }, container: { + batchUpgrade: '批次升級', + batchUpgradeLog: '查看日誌', + batchUpgradeCheck: '核對任務', + batchUpgradeRetry: '重試提交失敗的組', + batchUpgradeSubmitting: '正在提交', + batchUpgradeSubmitFailed: '提交失敗', + batchUpgradeUnknown: '提交結果待確認,請核對任務,勿重複提交', + batchUpgradeCanceled: '已取消', + batchUpgradeWaiting: '等待執行', + batchUpgradeRunning: '正在升級', + batchUpgradeSubmitted: '已提交升級任務', createByCommand: '指令建立', importContainerBackupTip: '匯入容器備份檔案,僅支援 .tar.gz 格式', importComposeBackupTip: '匯入編排備份檔案,僅支援 .tar.gz 格式', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index ebb02761d..f2114043b 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1995,6 +1995,17 @@ const message = { missing: '不存在', }, container: { + batchUpgrade: '批量升级', + batchUpgradeLog: '查看日志', + batchUpgradeCheck: '核对任务', + batchUpgradeRetry: '重试提交失败的组', + batchUpgradeSubmitting: '正在提交', + batchUpgradeSubmitFailed: '提交失败', + batchUpgradeUnknown: '提交结果待确认,请核对任务,勿重复提交', + batchUpgradeCanceled: '已取消', + batchUpgradeWaiting: '等待执行', + batchUpgradeRunning: '正在升级', + batchUpgradeSubmitted: '已提交升级任务', createByCommand: '命令创建', importContainerBackupTip: '导入容器备份文件,仅支持 .tar.gz 格式', importComposeBackupTip: '导入编排备份文件,仅支持 .tar.gz 格式', diff --git a/frontend/src/views/container/container/index.vue b/frontend/src/views/container/container/index.vue index e80bea076..e7049ced7 100644 --- a/frontend/src/views/container/container/index.vue +++ b/frontend/src/views/container/container/index.vue @@ -356,6 +356,7 @@ @change="toggleSelection" > + }; const onOperate = async (op: string, row: Container.ContainerInfo | null) => { + if (op === 'upgrade') { + dialogUpgradeRef.value.acceptParams(selects.value); + return; + } let opList = row ? [row] : selects.value; let msg = i18n.global.t('container.operatorHelper', [i18n.global.t('container.' + op)]); batchNames.value = []; diff --git a/frontend/src/views/container/container/upgrade/index.vue b/frontend/src/views/container/container/upgrade/index.vue index 0b18ed76b..6919e6530 100644 --- a/frontend/src/views/container/container/upgrade/index.vue +++ b/frontend/src/views/container/container/upgrade/index.vue @@ -1,185 +1,346 @@ + +