mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-06 13:27:43 +08:00
feat: Support batch upgrades for container images (#9915)
This commit is contained in:
parent
c49c16cbb5
commit
b156ef9476
16 changed files with 206 additions and 53 deletions
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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.',
|
||||||
|
|
|
@ -783,6 +783,8 @@ const message = {
|
||||||
upgradeWarning2: 'アップグレード操作では、コンテナを再構築する必要があります。続けたいですか?',
|
upgradeWarning2: 'アップグレード操作では、コンテナを再構築する必要があります。続けたいですか?',
|
||||||
oldImage: '現在の画像',
|
oldImage: '現在の画像',
|
||||||
targetImage: 'ターゲット画像',
|
targetImage: 'ターゲット画像',
|
||||||
|
sameImageContainer: '同一イメージコンテナ',
|
||||||
|
sameImageHelper: '同一イメージを使用するコンテナは選択後一括アップグレード可能',
|
||||||
imageLoadErr: 'コンテナの画像名は検出されません',
|
imageLoadErr: 'コンテナの画像名は検出されません',
|
||||||
appHelper:
|
appHelper:
|
||||||
'このコンテナはアプリストアから取得されたものであり、アップグレードによってサービスが利用不可になる可能性があります。',
|
'このコンテナはアプリストアから取得されたものであり、アップグレードによってサービスが利用不可になる可能性があります。',
|
||||||
|
|
|
@ -777,6 +777,8 @@ const message = {
|
||||||
upgradeWarning2:
|
upgradeWarning2:
|
||||||
'업그레이드 작업은 컨테이너를 재빌드해야 하며, 비지속적인 데이터가 손실됩니다. 계속하시겠습니까?',
|
'업그레이드 작업은 컨테이너를 재빌드해야 하며, 비지속적인 데이터가 손실됩니다. 계속하시겠습니까?',
|
||||||
oldImage: '현재 이미지',
|
oldImage: '현재 이미지',
|
||||||
|
sameImageContainer: '동일 이미지 컨테이너',
|
||||||
|
sameImageHelper: '동일한 이미지를 사용하는 컨테이너는 선택 후 일괄 업그레이드 가능',
|
||||||
targetImage: '대상 이미지',
|
targetImage: '대상 이미지',
|
||||||
imageLoadErr: '컨테이너에 대한 이미지 이름이 감지되지 않았습니다.',
|
imageLoadErr: '컨테이너에 대한 이미지 이름이 감지되지 않았습니다.',
|
||||||
appHelper: '이 컨테이너는 앱 스토어에서 왔으며 업그레이드 시 서비스가 중단될 수 있습니다.',
|
appHelper: '이 컨테이너는 앱 스토어에서 왔으며 업그레이드 시 서비스가 중단될 수 있습니다.',
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.',
|
||||||
|
|
|
@ -793,6 +793,8 @@ const message = {
|
||||||
upgradeWarning2:
|
upgradeWarning2:
|
||||||
'Операция обновления требует пересборки контейнера, все несохраненные данные будут потеряны. Хотите продолжить?',
|
'Операция обновления требует пересборки контейнера, все несохраненные данные будут потеряны. Хотите продолжить?',
|
||||||
oldImage: 'Текущий образ',
|
oldImage: 'Текущий образ',
|
||||||
|
sameImageContainer: 'Контейнеры с одинаковым образом',
|
||||||
|
sameImageHelper: 'Контейнеры, использующие один образ, можно массово обновить после выбора',
|
||||||
targetImage: 'Целевой образ',
|
targetImage: 'Целевой образ',
|
||||||
imageLoadErr: 'Не обнаружено имя образа для контейнера',
|
imageLoadErr: 'Не обнаружено имя образа для контейнера',
|
||||||
appHelper: 'Контейнер происходит из магазина приложений, и обновление может сделать сервис недоступным.',
|
appHelper: 'Контейнер происходит из магазина приложений, и обновление может сделать сервис недоступным.',
|
||||||
|
|
|
@ -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.',
|
||||||
|
|
|
@ -770,6 +770,8 @@ const message = {
|
||||||
upgradeHelper: '倉庫名稱/鏡像名稱:鏡像版本',
|
upgradeHelper: '倉庫名稱/鏡像名稱:鏡像版本',
|
||||||
upgradeWarning2: '升級操作需要重建容器,任何未持久化的數據將會丟失,是否繼續?',
|
upgradeWarning2: '升級操作需要重建容器,任何未持久化的數據將會丟失,是否繼續?',
|
||||||
oldImage: '當前鏡像',
|
oldImage: '當前鏡像',
|
||||||
|
sameImageContainer: '同鏡像容器',
|
||||||
|
sameImageHelper: '同鏡像容器可勾選後批量升級',
|
||||||
targetImage: '目標鏡像',
|
targetImage: '目標鏡像',
|
||||||
imageLoadErr: '未檢測到容器的鏡像名稱',
|
imageLoadErr: '未檢測到容器的鏡像名稱',
|
||||||
appHelper: '該容器來源於應用商店,升級可能導致該服務不可用',
|
appHelper: '該容器來源於應用商店,升級可能導致該服務不可用',
|
||||||
|
|
|
@ -769,6 +769,8 @@ const message = {
|
||||||
upgradeHelper: '仓库名称/镜像名称:镜像版本',
|
upgradeHelper: '仓库名称/镜像名称:镜像版本',
|
||||||
upgradeWarning2: '升级操作需要重建容器,任何未持久化的数据将会丢失,是否继续?',
|
upgradeWarning2: '升级操作需要重建容器,任何未持久化的数据将会丢失,是否继续?',
|
||||||
oldImage: '当前镜像',
|
oldImage: '当前镜像',
|
||||||
|
sameImageContainer: '同镜像容器',
|
||||||
|
sameImageHelper: '同镜像容器可勾选后批量升级',
|
||||||
targetImage: '目标镜像',
|
targetImage: '目标镜像',
|
||||||
imageLoadErr: '未检测到容器的镜像名称',
|
imageLoadErr: '未检测到容器的镜像名称',
|
||||||
appHelper: '该容器来源于应用商店,升级可能导致该服务不可用',
|
appHelper: '该容器来源于应用商店,升级可能导致该服务不可用',
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue