diff --git a/agent/app/api/v2/container.go b/agent/app/api/v2/container.go index c25e05209..54030e895 100644 --- a/agent/app/api/v2/container.go +++ b/agent/app/api/v2/container.go @@ -57,7 +57,7 @@ func (b *BaseApi) LoadContainerUsers(c *gin.Context) { // @Summary List containers // @Accept json // @Produce json -// @Success 200 {array} string +// @Success 200 {array} dto.ContainerOptions // @Security ApiKeyAuth // @Security Timestamp // @Router /containers/list [post] @@ -65,6 +65,23 @@ func (b *BaseApi) ListContainer(c *gin.Context) { 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 // @Summary Load containers status // @Accept json diff --git a/agent/app/dto/container.go b/agent/app/dto/container.go index 0a8ddbc33..0d119db27 100644 --- a/agent/app/dto/container.go +++ b/agent/app/dto/container.go @@ -107,10 +107,10 @@ type ContainerCreateByCommand struct { } type ContainerUpgrade struct { - TaskID string `json:"taskID"` - Name string `json:"name" validate:"required"` - Image string `json:"image" validate:"required"` - ForcePull bool `json:"forcePull"` + TaskID string `json:"taskID"` + Names []string `json:"names" validate:"required"` + Image string `json:"image" validate:"required"` + ForcePull bool `json:"forcePull"` } type ContainerListStats struct { diff --git a/agent/app/service/container.go b/agent/app/service/container.go index 2cbb6e8de..0902568b7 100644 --- a/agent/app/service/container.go +++ b/agent/app/service/container.go @@ -54,6 +54,7 @@ type ContainerService struct{} type IContainerService interface { Page(req dto.PageContainer) (int64, interface{}, error) List() []dto.ContainerOptions + ListByImage(imageName string) []dto.ContainerOptions LoadStatus() (dto.ContainerStatus, error) PageNetwork(req dto.SearchWithPage) (int64, interface{}, error) ListNetwork() ([]dto.Options, error) @@ -246,6 +247,33 @@ func (u *ContainerService) List() []dto.ContainerOptions { 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) { var data dto.ContainerStatus client, err := docker.NewDockerClient() @@ -683,6 +711,12 @@ func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error { 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) if err != nil { 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() ctx := context.Background() - oldContainer, err := client.ContainerInspect(ctx, req.Name) - if err != nil { - return err - } - taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpgrade, task.TaskScopeContainer, req.TaskID, 1) + taskItem, err := task.NewTaskWithOps(req.Image, task.TaskUpgrade, task.TaskScopeImage, req.TaskID, 1) if err != nil { global.LOG.Errorf("new task for create container failed, err: %v", err) return err } go func() { 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 { @@ -766,38 +797,58 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error { } return nil }, nil) - - taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", req.Name), func(t *task.Task) error { - config := oldContainer.Config - config.Image = req.Image - hostConf := oldContainer.HostConfig - var networkConf network.NetworkingConfig - if oldContainer.NetworkSettings != nil { - for networkKey := range oldContainer.NetworkSettings.Networks { - networkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}} - break + for _, item := range req.Names { + var oldContainer container.InspectResponse + taskItem.AddSubTask(i18n.GetWithName("ContainerLoadInfo", item), func(t *task.Task) error { + taskItem.Logf("----------------- %s -----------------", item) + oldContainer, err = client.ContainerInspect(ctx, item) + if err != nil { + return err } - } - err := client.ContainerRemove(ctx, req.Name, container.RemoveOptions{Force: true}) - taskItem.LogWithStatus(i18n.GetWithName("ContainerRemoveOld", req.Name), err) - if err != nil { - return err - } + inspected, err := client.ImageInspect(ctx, req.Image) + if err != nil { + return fmt.Errorf("inspect image failed, err: %v", 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) - if err != nil { - taskItem.Log(i18n.GetMsgByKey("ContainerRecreate")) - reCreateAfterUpdate(req.Name, 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) + taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", item), func(t *task.Task) error { + oldContainer.Config.Cmd = nil + config := oldContainer.Config + config.Image = req.Image + hostConf := oldContainer.HostConfig + var networkConf network.NetworkingConfig + if oldContainer.NetworkSettings != nil { + for networkKey := range oldContainer.NetworkSettings.Networks { + networkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}} + break + } + } + 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 { global.LOG.Error(err.Error()) } @@ -1692,3 +1743,23 @@ func loadContainerPortForInfo(itemPorts []container.Port) []dto.PortHelper { } 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 +} diff --git a/agent/router/ro_container.go b/agent/router/ro_container.go index cc8821bc2..724fec76d 100644 --- a/agent/router/ro_container.go +++ b/agent/router/ro_container.go @@ -21,6 +21,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) { baRouter.POST("/info", baseApi.ContainerInfo) baRouter.POST("/search", baseApi.SearchContainer) baRouter.POST("/list", baseApi.ListContainer) + baRouter.POST("/list/byimage", baseApi.ListContainerByImage) baRouter.GET("/status", baseApi.LoadContainerStatus) baRouter.GET("/list/stats", baseApi.ContainerListStats) baRouter.GET("/search/log", baseApi.ContainerStreamLogs) diff --git a/frontend/src/api/interface/container.ts b/frontend/src/api/interface/container.ts index e80b17714..1260d5caf 100644 --- a/frontend/src/api/interface/container.ts +++ b/frontend/src/api/interface/container.ts @@ -45,6 +45,10 @@ export namespace Container { imageSize: number; } + export interface ContainerOption { + name: string; + state: string; + } export interface ResourceLimit { cpu: number; memory: number; @@ -86,7 +90,7 @@ export namespace Container { } export interface ContainerUpgrade { taskID: string; - name: string; + names: Array; image: string; forcePull: boolean; } diff --git a/frontend/src/api/modules/container.ts b/frontend/src/api/modules/container.ts index 5d246147f..9421d87ec 100644 --- a/frontend/src/api/modules/container.ts +++ b/frontend/src/api/modules/container.ts @@ -7,7 +7,10 @@ export const searchContainer = (params: Container.ContainerSearch) => { return http.post>(`/containers/search`, params, TimeoutEnum.T_40S); }; export const listContainer = () => { - return http.post>(`/containers/list`, {}); + return http.post>(`/containers/list`, {}); +}; +export const listContainerByImage = (image: string) => { + return http.post>(`/containers/list/byimage`, { name: image }); }; export const loadContainerUsers = (name: string) => { return http.post>(`/containers/users`, { name: name }); diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index c9d321c9c..e737a3680 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -805,6 +805,8 @@ const message = { upgradeWarning2: 'The upgrade operation requires rebuilding the container, any unpersisted data will be lost. Do you wish to continue?', oldImage: 'Current image', + sameImageContainer: 'Same-image containers', + sameImageHelper: 'Containers using the same image can be batch upgraded after selection', targetImage: 'Target image', imageLoadErr: 'No image name detected for the container', appHelper: 'The container comes from the app store, and upgrading may make the service unavailable.', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 619802888..a101acdb0 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -783,6 +783,8 @@ const message = { upgradeWarning2: 'アップグレード操作では、コンテナを再構築する必要があります。続けたいですか?', oldImage: '現在の画像', targetImage: 'ターゲット画像', + sameImageContainer: '同一イメージコンテナ', + sameImageHelper: '同一イメージを使用するコンテナは選択後一括アップグレード可能', imageLoadErr: 'コンテナの画像名は検出されません', appHelper: 'このコンテナはアプリストアから取得されたものであり、アップグレードによってサービスが利用不可になる可能性があります。', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index eeb9edf9d..9b40bfa54 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -777,6 +777,8 @@ const message = { upgradeWarning2: '업그레이드 작업은 컨테이너를 재빌드해야 하며, 비지속적인 데이터가 손실됩니다. 계속하시겠습니까?', oldImage: '현재 이미지', + sameImageContainer: '동일 이미지 컨테이너', + sameImageHelper: '동일한 이미지를 사용하는 컨테이너는 선택 후 일괄 업그레이드 가능', targetImage: '대상 이미지', imageLoadErr: '컨테이너에 대한 이미지 이름이 감지되지 않았습니다.', appHelper: '이 컨테이너는 앱 스토어에서 왔으며 업그레이드 시 서비스가 중단될 수 있습니다.', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 1b8f4162b..0ade21181 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -798,6 +798,8 @@ const message = { upgradeWarning2: 'Operasi peningkatan memerlukan pembinaan semula kontena, sebarang data yang tidak disimpan akan hilang. Adakah anda mahu meneruskan?', oldImage: 'Imej semasa', + sameImageContainer: 'Kontena imej sama', + sameImageHelper: 'Kontena yang menggunakan imej sama boleh dinaik taraf secara berkumpulan setelah dipilih', targetImage: 'Imej sasaran', imageLoadErr: 'Tiada nama imej dikesan untuk kontena', appHelper: diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 9c8420799..9c2c88d11 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -794,6 +794,8 @@ const message = { upgradeWarning2: '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', + 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', 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.', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index add1c566d..f735ffc68 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -793,6 +793,8 @@ const message = { upgradeWarning2: 'Операция обновления требует пересборки контейнера, все несохраненные данные будут потеряны. Хотите продолжить?', oldImage: 'Текущий образ', + sameImageContainer: 'Контейнеры с одинаковым образом', + sameImageHelper: 'Контейнеры, использующие один образ, можно массово обновить после выбора', targetImage: 'Целевой образ', imageLoadErr: 'Не обнаружено имя образа для контейнера', appHelper: 'Контейнер происходит из магазина приложений, и обновление может сделать сервис недоступным.', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index e441c47d6..a370196fb 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -815,6 +815,8 @@ const message = { upgradeWarning2: 'Yükseltme işlemi konteynerin yeniden oluşturulmasını gerektirir, kalıcı olmayan tüm veriler kaybedilecektir. Devam etmek istiyor musunuz?', oldImage: 'Mevcut imaj', + sameImageContainer: 'Aynı imajlı konteynerler', + sameImageHelper: 'Aynı imajı kullanan konteynerlar seçilerek toplu şekilde güncellenebilir', targetImage: 'Hedef imaj', imageLoadErr: 'Konteyner için imaj adı algılanmadı', appHelper: 'Konteyner uygulama mağazasından geliyor ve yükseltme hizmeti kullanılamaz hale getirebilir.', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 16bc7e01a..ce5b3f595 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -770,6 +770,8 @@ const message = { upgradeHelper: '倉庫名稱/鏡像名稱:鏡像版本', upgradeWarning2: '升級操作需要重建容器,任何未持久化的數據將會丟失,是否繼續?', oldImage: '當前鏡像', + sameImageContainer: '同鏡像容器', + sameImageHelper: '同鏡像容器可勾選後批量升級', targetImage: '目標鏡像', imageLoadErr: '未檢測到容器的鏡像名稱', appHelper: '該容器來源於應用商店,升級可能導致該服務不可用', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 879ddbe44..94c2e7019 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -769,6 +769,8 @@ const message = { upgradeHelper: '仓库名称/镜像名称:镜像版本', upgradeWarning2: '升级操作需要重建容器,任何未持久化的数据将会丢失,是否继续?', oldImage: '当前镜像', + sameImageContainer: '同镜像容器', + sameImageHelper: '同镜像容器可勾选后批量升级', targetImage: '目标镜像', imageLoadErr: '未检测到容器的镜像名称', appHelper: '该容器来源于应用商店,升级可能导致该服务不可用', diff --git a/frontend/src/views/container/container/upgrade/index.vue b/frontend/src/views/container/container/upgrade/index.vue index 08c9a7641..1a35c8cef 100644 --- a/frontend/src/views/container/container/upgrade/index.vue +++ b/frontend/src/views/container/container/upgrade/index.vue @@ -1,11 +1,5 @@