diff --git a/agent/app/api/v2/container_image_check_update.go b/agent/app/api/v2/container_image_check_update.go new file mode 100644 index 000000000..7e5d4c14a --- /dev/null +++ b/agent/app/api/v2/container_image_check_update.go @@ -0,0 +1,43 @@ +package v2 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" + "github.com/1Panel-dev/1Panel/agent/app/service" + "github.com/gin-gonic/gin" +) + +func (b *BaseApi) GetContainerImageCheckUpdateStatus(c *gin.Context) { + status, err := service.GetContainerImageCheckUpdateStatus() + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, status) +} +func (b *BaseApi) ContainerImageCheckUpdate(c *gin.Context) { + taskID, err := service.StartContainerImageCheckUpdate() + if err != nil { + helper.InternalServer(c, err) + return + } + status, err := service.GetContainerImageCheckUpdateStatus() + if err != nil { + helper.InternalServer(c, err) + return + } + status.TaskID = taskID + helper.SuccessWithData(c, status) +} +func (b *BaseApi) SetContainerImageCheckUpdateSchedule(c *gin.Context) { + var req struct { + Enabled *bool `json:"enabled" validate:"required"` + } + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := service.SetContainerImageCheckUpdateSchedule(*req.Enabled); err != nil { + helper.InternalServer(c, err) + return + } + b.GetContainerImageCheckUpdateStatus(c) +} diff --git a/agent/app/dto/container.go b/agent/app/dto/container.go index e3d1502d4..cfb6d7bae 100644 --- a/agent/app/dto/container.go +++ b/agent/app/dto/container.go @@ -21,13 +21,16 @@ type InspectReq struct { } type ContainerInfo struct { - ContainerID string `json:"containerID"` - Name string `json:"name"` - ImageId string `json:"imageID"` - ImageName string `json:"imageName"` - CreateTime string `json:"createTime"` - State string `json:"state"` - RunTime string `json:"runTime"` + LatestVersion string `json:"latestVersion"` + TargetImage string `json:"targetImage"` + HasUpdate bool `json:"hasUpdate"` + ContainerID string `json:"containerID"` + Name string `json:"name"` + ImageId string `json:"imageID"` + ImageName string `json:"imageName"` + CreateTime string `json:"createTime"` + State string `json:"state"` + RunTime string `json:"runTime"` Network []string `json:"network"` Ports []string `json:"ports"` diff --git a/agent/app/model/container_image_check_update.go b/agent/app/model/container_image_check_update.go new file mode 100644 index 000000000..c7f715532 --- /dev/null +++ b/agent/app/model/container_image_check_update.go @@ -0,0 +1,20 @@ +package model + +import "time" + +// ContainerImageCheckUpdate is a cache tied to the exact image used by a container. +type ContainerImageCheckUpdate struct { + ContainerID string `gorm:"primaryKey" json:"containerID"` + ImageID string `json:"imageID"` + ImageName string `json:"imageName"` + LatestVersion string `json:"latestVersion"` + TargetImage string `json:"targetImage"` + TargetImageID string `json:"-"` + HasUpdate bool `gorm:"not null;default:false" json:"hasUpdate"` + CheckedAt time.Time `json:"checkedAt"` + CheckStatus string `json:"checkStatus"` + CheckError string `json:"checkError"` +} + +// TableName preserves the table created by the original migration. +func (ContainerImageCheckUpdate) TableName() string { return "container_image_updates" } diff --git a/agent/app/service/container.go b/agent/app/service/container.go index 0c28ebe87..e863d0379 100644 --- a/agent/app/service/container.go +++ b/agent/app/service/container.go @@ -140,6 +140,29 @@ func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int backData = records[start:end] } + // Batch-load persisted checks; never contact Docker Hub while rendering the list. + ids := make([]string, 0, len(backData)) + for _, row := range backData { + ids = append(ids, row.ContainerID) + } + if len(ids) > 0 { + var checks []model.ContainerImageCheckUpdate + if err := global.DB.Where("container_id IN ?", ids).Find(&checks).Error; err != nil { + return 0, nil, err + } + byID := make(map[string]model.ContainerImageCheckUpdate, len(checks)) + for _, check := range checks { + byID[check.ContainerID] = check + } + for i := range backData { + check, ok := byID[backData[i].ContainerID] + if ok && sameContainerImage(check.ImageID, backData[i].ImageId) && check.ImageName == backData[i].ImageName { + backData[i].LatestVersion = check.LatestVersion + backData[i].TargetImage = check.TargetImage + backData[i].HasUpdate = check.HasUpdate + } + } + } for i := 0; i < len(backData); i++ { install, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(backData[i].Name)) if install.ID > 0 { diff --git a/agent/app/service/container_image_check_update.go b/agent/app/service/container_image_check_update.go new file mode 100644 index 000000000..c7b99c924 --- /dev/null +++ b/agent/app/service/container_image_check_update.go @@ -0,0 +1,411 @@ +package service + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + "sync" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/task" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/1Panel-dev/1Panel/agent/utils/dockerhub" + "github.com/docker/cli/cli/config" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" + "github.com/docker/docker/errdefs" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +const imageCheckUpdateSetting = "ContainerImageUpdateEnabled" + +type ImageCheckUpdateStatus struct { + ProxyRequired bool `json:"proxyRequired"` + TaskID string `json:"taskID"` + Enabled bool `json:"enabled"` + Running bool `json:"running"` + Checked int `json:"checked"` + Updates int `json:"updates"` + Failed int `json:"failed"` + Skipped int `json:"skipped"` + Error string `json:"error"` + FinishedAt int64 `json:"finishedAt"` +} + +var imageCheckUpdateState struct { + sync.Mutex + status ImageCheckUpdateStatus +} + +func GetContainerImageCheckUpdateStatus() (ImageCheckUpdateStatus, error) { + var setting model.Setting + if err := global.DB.Where("key = ?", imageCheckUpdateSetting).First(&setting).Error; err != nil { + return ImageCheckUpdateStatus{}, err + } + httpClient, proxyErr := dockerhub.NewHTTPClient() + proxyRequired := false + if proxyErr != nil { + if business, ok := proxyErr.(buserr.BusinessError); ok && business.Msg == "ErrImageCheckProxyRequired" { + proxyRequired = true + } else { + return ImageCheckUpdateStatus{}, proxyErr + } + } + if httpClient != nil && httpClient.Transport != nil { + httpClient.CloseIdleConnections() + } + imageCheckUpdateState.Lock() + defer imageCheckUpdateState.Unlock() + status := imageCheckUpdateState.status + status.Enabled = setting.Value == "Enable" + status.ProxyRequired = proxyRequired + return status, nil +} +func SetContainerImageCheckUpdateSchedule(enabled bool) error { + value := "Disable" + if enabled { + httpClient, err := dockerhub.NewHTTPClient() + if err != nil { + return err + } + if httpClient.Transport != nil { + httpClient.CloseIdleConnections() + } + value = "Enable" + } + return global.DB.Model(&model.Setting{}).Where("key = ?", imageCheckUpdateSetting).Update("value", value).Error +} + +// StartContainerImageCheckUpdate coalesces manual and scheduled runs on this node. +func StartContainerImageCheckUpdate() (string, error) { + imageCheckUpdateState.Lock() + defer imageCheckUpdateState.Unlock() + if imageCheckUpdateState.status.Running { + return imageCheckUpdateState.status.TaskID, nil + } + taskItem, err := task.NewTask(i18n.GetMsgByKey("ContainerImageCheckUpdate"), task.TaskScan, task.TaskScopeImage, "", 0) + if err != nil { + return "", err + } + // Persist before returning the ID so the log drawer can immediately find it. + if err := global.TaskDB.Save(taskItem.Task).Error; err != nil { + _ = taskItem.Logger.Out.(io.Closer).Close() + taskItem.DeleteLogFile() + if cancel, ok := global.LoadTaskCancel(taskItem.TaskID); ok { + cancel() + } + global.RemoveTaskCancel(taskItem.TaskID) + return "", err + } + imageCheckUpdateState.status = ImageCheckUpdateStatus{Running: true, TaskID: taskItem.TaskID} + go func() { + result := ImageCheckUpdateStatus{TaskID: taskItem.TaskID} + defer func() { + result.FinishedAt = time.Now().UnixMilli() + imageCheckUpdateState.Lock() + imageCheckUpdateState.status = result + imageCheckUpdateState.Unlock() + }() + // The action owns its deadline so the task runner waits for cleanup and logs. + taskItem.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerImageCheckUpdate"), func(t *task.Task) (err error) { + defer func() { + if recovered := recover(); recovered != nil { + err = fmt.Errorf("image update check failed: %v", recovered) + } + t.Log(i18n.GetMsgWithMap("ContainerImageCheckSummary", map[string]interface{}{ + "checked": result.Checked, "updates": result.Updates, "failed": result.Failed, "skipped": result.Skipped, + })) + }() + ctx, cancel := context.WithTimeout(t.TaskCtx, 30*time.Minute) + defer cancel() + if err = runContainerImageCheckUpdate(ctx, &result, t.Log); err != nil { + return err + } + if result.Failed > 0 { + return fmt.Errorf("%s", i18n.GetMsgWithMap("ContainerImageCheckFailures", map[string]interface{}{"count": result.Failed})) + } + return nil + }, nil, 0, 0) + if err := taskItem.Execute(); err != nil { + result.Error = err.Error() + global.LOG.Errorf("container image check: %s", err) + } + }() + return taskItem.TaskID, nil +} + +// These caches live only for one check. Remote results do not depend on a +// container's local image ID, which is compared separately for every baseline. +type imageCheckUpdateRepository struct { + client imageCheckUpdateHubClient + err error + tags []string + tagsLoaded bool + tagsErr error +} +type imageCheckUpdateTargetKey struct { + repository, tag string + platform dockerhub.Platform +} +type imageCheckUpdateTargetResult struct { + target dockerhub.Target + compatible bool + err error +} +type imageCheckUpdateResult struct { + record model.ContainerImageCheckUpdate + err error +} +type imageCheckUpdateCache struct { + username, password string + httpClient *http.Client + repositories map[string]*imageCheckUpdateRepository + targets map[imageCheckUpdateTargetKey]imageCheckUpdateTargetResult +} + +func runContainerImageCheckUpdate(ctx context.Context, result *ImageCheckUpdateStatus, logger func(string)) error { + httpClient, err := dockerhub.NewHTTPClient() + if err != nil { + return err + } + if httpClient.Transport != nil { + defer httpClient.CloseIdleConnections() + } + log := func(key string, values map[string]interface{}) { + if logger != nil { + logger(i18n.GetMsgWithMap(key, values)) + } + } + cli, err := newImageCheckUpdateDockerClient() + if err != nil { + return err + } + defer cli.Close() + containers, err := cli.ContainerList(ctx, container.ListOptions{All: true}) + if err != nil { + return err + } + log("ContainerImageCheckScope", map[string]interface{}{"count": len(containers)}) + // Load old results once, rather than issuing one SELECT for every failed check. + var previousRows []model.ContainerImageCheckUpdate + if err := global.DB.WithContext(ctx).Find(&previousRows).Error; err != nil { + return err + } + previous := make(map[string]model.ContainerImageCheckUpdate, len(previousRows)) + for _, row := range previousRows { + previous[row.ContainerID] = row + } + cache := imageCheckUpdateCache{httpClient: httpClient, repositories: map[string]*imageCheckUpdateRepository{}, targets: map[imageCheckUpdateTargetKey]imageCheckUpdateTargetResult{}} + cfg := config.LoadDefaultConfigFile(io.Discard) + if cfg != nil { + for _, host := range []string{"https://index.docker.io/v1/", "docker.io", "registry-1.docker.io"} { + auth, e := cfg.GetAuthConfig(host) + if e == nil && auth.Username != "" { + cache.username, cache.password = auth.Username, auth.Password + break + } + } + } + checks := map[string]imageCheckUpdateResult{} + ids := make([]string, 0, len(containers)) + names := make(map[string]string, len(containers)) + pending := make([]model.ContainerImageCheckUpdate, 0, 100) + for _, item := range containers { + if err := ctx.Err(); err != nil { + return err + } + ids = append(ids, item.ID) + name := item.ID + if len(item.Names) > 0 { + name = strings.TrimPrefix(item.Names[0], "/") + } + names[item.ID] = name + log("ContainerImageCheckItem", map[string]interface{}{"name": name, "image": item.Image}) + repository, tag, supported := dockerhub.Reference(item.Image) + key := repository + ":" + tag + "@" + item.ImageID + entry, found := checks[key] + if !found { + entry.record, entry.err = checkContainerImageUpdate(ctx, cli, item, repository, tag, supported, &cache) + checks[key] = entry + } + record := entry.record + record.ContainerID, record.ImageName = item.ID, item.Image + if entry.err != nil { + record.CheckStatus, record.CheckError = "failed", entry.err.Error() + old := previous[item.ID] + if old.ImageID == item.ImageID && old.ImageName == item.Image { + record.LatestVersion, record.TargetImage, record.TargetImageID, record.HasUpdate = old.LatestVersion, old.TargetImage, old.TargetImageID, old.HasUpdate + } + } + pending = append(pending, record) + if len(pending) == 100 { + if err := saveContainerImageCheckUpdates(ctx, cli, pending, names, result, log); err != nil { + return err + } + pending = pending[:0] + } + } + if err := saveContainerImageCheckUpdates(ctx, cli, pending, names, result, log); err != nil { + return err + } + query := global.DB.WithContext(ctx).Where("1 = 1") + if len(ids) > 0 { + query = query.Where("container_id NOT IN ?", ids) + } + return query.Delete(&model.ContainerImageCheckUpdate{}).Error +} + +func checkContainerImageUpdate(ctx context.Context, cli imageCheckUpdateDockerClient, item container.Summary, repository, tag string, supported bool, cache *imageCheckUpdateCache) (model.ContainerImageCheckUpdate, error) { + record := model.ContainerImageCheckUpdate{ImageID: item.ImageID, ImageName: item.Image, CheckStatus: "success", CheckedAt: time.Now()} + if !supported { + record.CheckStatus = "skipped" + return record, nil + } + inspected, err := cli.ImageInspect(ctx, item.ImageID) + if err != nil { + return record, err + } + repo, loaded := cache.repositories[repository] + if !loaded { + repo = &imageCheckUpdateRepository{} + repo.client, repo.err = newImageCheckUpdateHubClient(ctx, repository, cache.username, cache.password, cache.httpClient) + cache.repositories[repository] = repo + } + if repo.err != nil { + return record, repo.err + } + if dockerhub.NeedsTags(tag) { + if !repo.tagsLoaded { + repo.tags, repo.tagsErr = repo.client.Tags(ctx) + repo.tagsLoaded = true + } + if repo.tagsErr != nil { + return record, repo.tagsErr + } + } + platform := dockerhub.Platform{OS: inspected.Os, Architecture: inspected.Architecture, Variant: inspected.Variant} + for _, candidate := range dockerhub.Candidates(tag, repo.tags) { + key := imageCheckUpdateTargetKey{repository: repository, tag: candidate, platform: platform} + remote, loaded := cache.targets[key] + if !loaded { + remote.target, remote.compatible, remote.err = repo.client.Resolve(ctx, candidate, platform) + cache.targets[key] = remote + } + if remote.err != nil { + return record, remote.err + } + if !remote.compatible { + continue + } + record.LatestVersion = remote.target.Tag + record.TargetImage = "docker.io/" + repository + ":" + remote.target.Tag + record.TargetImageID = remote.target.ConfigDigest + // Both versioned and floating tags can be republished with new image content. + record.HasUpdate = remote.target.Tag != tag || remote.target.ConfigDigest != inspected.ID + return record, nil + } + return record, fmt.Errorf("no compatible image for %s/%s", platform.OS, platform.Architecture) +} + +// Revalidate immediately before each small write batch. Never hold a database +// transaction open while waiting for Docker or Docker Hub. +func saveContainerImageCheckUpdates(ctx context.Context, cli imageCheckUpdateDockerClient, pending []model.ContainerImageCheckUpdate, names map[string]string, result *ImageCheckUpdateStatus, log func(string, map[string]interface{})) error { + records := make([]model.ContainerImageCheckUpdate, 0, len(pending)) + for _, record := range pending { + current, err := cli.ContainerInspect(ctx, record.ContainerID) + if err != nil && !errdefs.IsNotFound(err) { + return err + } + if err != nil || current.Image != record.ImageID { + result.Skipped++ + log("ContainerImageCheckChanged", map[string]interface{}{"name": names[record.ContainerID]}) + continue + } + records = append(records, record) + } + if len(records) == 0 { + return nil + } + if err := global.DB.WithContext(ctx).Clauses(clause.OnConflict{UpdateAll: true}).CreateInBatches(&records, 100).Error; err != nil { + return err + } + // Count and announce only results that were successfully persisted. + for _, record := range records { + values := map[string]interface{}{"name": names[record.ContainerID], "image": record.ImageName} + switch record.CheckStatus { + case "failed": + result.Failed++ + values["error"] = record.CheckError + log("ContainerImageCheckFailed", values) + case "skipped": + result.Skipped++ + log("ContainerImageCheckSkipped", values) + default: + result.Checked++ + if record.HasUpdate { + result.Updates++ + values["target"] = record.TargetImage + log("ContainerImageCheckAvailable", values) + } else { + log("ContainerImageCheckCurrent", values) + } + } + } + return nil +} + +// sameContainerImage guards cached results after external image changes. +func sameContainerImage(stored, displayed string) bool { + return strings.TrimPrefix(stored, "sha256:") == strings.TrimPrefix(displayed, "sha256:") +} + +// Reconcile only after Docker confirms the replacement container uses the target. +func reconcileContainerImageCheckUpdate(oldID, newID, imageID, imageName string) error { + return global.DB.Transaction(func(tx *gorm.DB) error { + var previous model.ContainerImageCheckUpdate + if err := tx.Where("container_id = ?", oldID).Find(&previous).Error; err != nil { + return err + } + if previous.ContainerID == "" { + return nil + } + if err := tx.Where("container_id = ?", oldID).Delete(&model.ContainerImageCheckUpdate{}).Error; err != nil { + return err + } + if previous.TargetImageID != imageID { + return nil + } + previous.ContainerID = newID + previous.ImageID = imageID + previous.ImageName = imageName + previous.HasUpdate = false + previous.CheckStatus = "success" + previous.CheckError = "" + previous.CheckedAt = time.Now() + return tx.Clauses(clause.OnConflict{UpdateAll: true}).Create(&previous).Error + }) +} + +type imageCheckUpdateDockerClient interface { + ContainerList(context.Context, container.ListOptions) ([]container.Summary, error) + ContainerInspect(context.Context, string) (container.InspectResponse, error) + ImageInspect(context.Context, string, ...client.ImageInspectOption) (image.InspectResponse, error) + Close() error +} +type imageCheckUpdateHubClient interface { + Tags(context.Context) ([]string, error) + Resolve(context.Context, string, dockerhub.Platform) (dockerhub.Target, bool, error) +} + +var newImageCheckUpdateDockerClient = func() (imageCheckUpdateDockerClient, error) { return docker.NewDockerClient() } +var newImageCheckUpdateHubClient = func(ctx context.Context, repository, username, password string, httpClient *http.Client) (imageCheckUpdateHubClient, error) { + return dockerhub.New(ctx, repository, username, password, httpClient) +} diff --git a/agent/app/service/container_update.go b/agent/app/service/container_update.go index 99c4f9325..03f841f9b 100644 --- a/agent/app/service/container_update.go +++ b/agent/app/service/container_update.go @@ -163,6 +163,11 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error { upgradeErrors = append(upgradeErrors, upgradeErr) return upgradeErr } + if updated, inspectErr := client.ContainerInspect(ctx, item); inspectErr == nil { + if cacheErr := reconcileContainerImageCheckUpdate(oldContainer.ID, updated.ID, updated.Image, updated.Config.Image); cacheErr != nil { + global.LOG.Warnf("refresh container image update state: %v", cacheErr) + } + } if cleanupErr != nil { t.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr)) } @@ -307,7 +312,6 @@ func switchContainer( return switchContainerWithRecovery(ctx, cli, name, oldContainer, createNew, tty, logger, false) } - const ( containerUpgradePullTimeout = 30 * time.Minute containerUpgradeReplaceTimeout = 15 * time.Minute diff --git a/agent/cron/cron.go b/agent/cron/cron.go index 402cba256..5b554c2b5 100644 --- a/agent/cron/cron.go +++ b/agent/cron/cron.go @@ -77,7 +77,11 @@ func Run() { global.LOG.Errorf("generate random hour failed: %v", err) hourRand = big.NewInt(0) } - if _, err := global.Cron.AddJob(fmt.Sprintf("%v %v * * *", minuteRand.Int64(), hourRand.Int64()), job.NewAppStoreJob()); err != nil { + appSyncSchedule := fmt.Sprintf("%v %v * * *", minuteRand.Int64(), hourRand.Int64()) + if _, err := global.Cron.AddJob(appSyncSchedule, job.NewContainerImageCheckUpdateJob()); err != nil { + global.LOG.Errorf("cannot add container image update cron job: %v", err) + } + if _, err := global.Cron.AddJob(appSyncSchedule, job.NewAppStoreJob()); err != nil { global.LOG.Errorf("can not add appstore corn job: %s", err.Error()) } if _, err := global.Cron.AddJob("0 3 */31 * *", job.NewBackupJob()); err != nil { diff --git a/agent/cron/job/container_image_check_update.go b/agent/cron/job/container_image_check_update.go new file mode 100644 index 000000000..d0edd0e3f --- /dev/null +++ b/agent/cron/job/container_image_check_update.go @@ -0,0 +1,24 @@ +package job + +import ( + "github.com/1Panel-dev/1Panel/agent/app/service" + "github.com/1Panel-dev/1Panel/agent/global" +) + +type containerImageCheckUpdate struct{} + +func NewContainerImageCheckUpdateJob() *containerImageCheckUpdate { + return &containerImageCheckUpdate{} +} +func (j *containerImageCheckUpdate) Run() { + status, err := service.GetContainerImageCheckUpdateStatus() + if err != nil { + global.LOG.Errorf("load container image update schedule: %v", err) + return + } + if status.Enabled { + if _, err := service.StartContainerImageCheckUpdate(); err != nil { + global.LOG.Errorf("start container image update check: %v", err) + } + } +} diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index c61e82476..ef3951e3a 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -802,3 +802,15 @@ ErrFeishuUpgradeMounts: "These mounts cannot be fully backed up or restored. Use AgentFeishuRecoveryDisabled: "Gateway recovered with Feishu disabled; the original configuration backup is retained." ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" +ContainerImageCheckUpdate: "Check for image updates" +ContainerImageCheckScope: "Checking all {{ .count }} containers on this node, regardless of pagination or filters" +ContainerImageCheckItem: "Checking container {{ .name }}, image {{ .image }}" +ContainerImageCheckFailed: "Container {{ .name }} check failed: {{ .error }}; retaining previous results matching the current image" +ContainerImageCheckSkipped: "Skipping {{ .name }}: not a tagged Docker Hub image, a local image, or pinned digest" +ContainerImageCheckAvailable: "Container {{ .name }} has an update: {{ .image }} -> {{ .target }}" +ContainerImageCheckCurrent: "No update found for container {{ .name }}" +ContainerImageCheckChanged: "Container {{ .name }} was removed or its image changed; result was not saved" +ContainerImageCheckSummary: "Check finished: {{ .checked }} checked, {{ .updates }} upgradable, {{ .failed }} failed, {{ .skipped }} skipped" +ContainerImageCheckFailures: "{{ .count }} container checks failed; see the task log" + +ErrImageCheckProxyRequired: "To check for image updates in the current runtime environment, please configure a proxy server first." diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index e64a97c1b..d8885abc8 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -802,3 +802,15 @@ ErrFeishuUpgradeMounts: "Estos montajes no permiten una copia y restauración co AgentFeishuRecoveryDisabled: "Puerta de enlace recuperada con Feishu desactivado; se conserva la copia de configuración original." ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" +ContainerImageCheckUpdate: "Buscar actualizaciones de imágenes" +ContainerImageCheckScope: "Comprobando los {{ .count }} contenedores de este nodo, sin aplicar paginación ni filtros" +ContainerImageCheckItem: "Comprobando el contenedor {{ .name }}, imagen {{ .image }}" +ContainerImageCheckFailed: "Error al comprobar el contenedor {{ .name }}: {{ .error }}; se conservan los resultados anteriores correspondientes a la imagen actual" +ContainerImageCheckSkipped: "Se omite {{ .name }}: no es una imagen de Docker Hub con etiqueta, es una imagen local o tiene un digest fijo" +ContainerImageCheckAvailable: "El contenedor {{ .name }} tiene una actualización: {{ .image }} → {{ .target }}" +ContainerImageCheckCurrent: "No se encontraron actualizaciones para el contenedor {{ .name }}" +ContainerImageCheckChanged: "El contenedor {{ .name }} se eliminó o su imagen cambió; no se guardó el resultado" +ContainerImageCheckSummary: "Comprobación finalizada: comprobados {{ .checked }}, actualizables {{ .updates }}, fallidos {{ .failed }}, omitidos {{ .skipped }}" +ContainerImageCheckFailures: "Falló la comprobación de {{ .count }} contenedores; consulte el registro de la tarea" + +ErrImageCheckProxyRequired: "Para comprobar actualizaciones de imágenes en el entorno actual, configura primero un servidor proxy." diff --git a/agent/i18n/lang/fa.yaml b/agent/i18n/lang/fa.yaml index d6716865e..0fa68f950 100644 --- a/agent/i18n/lang/fa.yaml +++ b/agent/i18n/lang/fa.yaml @@ -803,3 +803,15 @@ ErrFeishuUpgradeMounts: "این اتصال‌های ذخیره‌سازی کام AgentFeishuRecoveryDisabled: "دروازه بازیابی شد و Feishu غیرفعال است؛ پشتیبان تنظیمات اصلی حفظ شد." ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" +ContainerImageCheckUpdate: "بررسی به‌روزرسانی ایمیج‌ها" +ContainerImageCheckScope: "در حال بررسی همه {{ .count }} کانتینر این گره، بدون توجه به صفحه‌بندی و فیلترها" +ContainerImageCheckItem: "در حال بررسی کانتینر {{ .name }}، ایمیج {{ .image }}" +ContainerImageCheckFailed: "بررسی کانتینر {{ .name }} ناموفق بود: {{ .error }}؛ نتایج قبلیِ منطبق با ایمیج فعلی حفظ می‌شوند" +ContainerImageCheckSkipped: "کانتینر {{ .name }} رد شد: ایمیج تگ‌دار Docker Hub نیست، ایمیج محلی است یا digest ثابت دارد" +ContainerImageCheckAvailable: "برای کانتینر {{ .name }} به‌روزرسانی موجود است: {{ .image }} → {{ .target }}" +ContainerImageCheckCurrent: "به‌روزرسانی برای کانتینر {{ .name }} یافت نشد" +ContainerImageCheckChanged: "کانتینر {{ .name }} حذف شده یا ایمیج آن تغییر کرده است؛ نتیجه ذخیره نشد" +ContainerImageCheckSummary: "بررسی پایان یافت: بررسی‌شده {{ .checked }}، قابل ارتقا {{ .updates }}، ناموفق {{ .failed }}، ردشده {{ .skipped }}" +ContainerImageCheckFailures: "بررسی {{ .count }} کانتینر ناموفق بود؛ گزارش وظیفه را مشاهده کنید" + +ErrImageCheckProxyRequired: "برای بررسی به‌روزرسانی تصاویر در محیط فعلی، ابتدا یک سرور پروکسی پیکربندی کنید." diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index 451dee002..0f91320b6 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -802,3 +802,15 @@ ErrFeishuUpgradeMounts: "現在のマウントは完全なバックアップ・ AgentFeishuRecoveryDisabled: "ゲートウェイを復旧しました。Feishu は無効のままで、元の設定のバックアップは保持しています。" ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" +ContainerImageCheckUpdate: "イメージの更新を確認" +ContainerImageCheckScope: "ページ分割やフィルターに関係なく、このノードの全 {{ .count }} コンテナを確認します" +ContainerImageCheckItem: "コンテナ {{ .name }}、イメージ {{ .image }} を確認しています" +ContainerImageCheckFailed: "コンテナ {{ .name }} の確認に失敗しました:{{ .error }}。現在のイメージに対応する前回の結果を保持します" +ContainerImageCheckSkipped: "{{ .name }} をスキップ:タグ付きの Docker Hub イメージではないか、ローカルイメージまたは digest 固定のイメージです" +ContainerImageCheckAvailable: "コンテナ {{ .name }} は更新可能です:{{ .image }} → {{ .target }}" +ContainerImageCheckCurrent: "コンテナ {{ .name }} の更新は見つかりませんでした" +ContainerImageCheckChanged: "コンテナ {{ .name }} が削除されたか、イメージが変更されたため、結果は保存されませんでした" +ContainerImageCheckSummary: "確認終了:成功 {{ .checked }}、更新可能 {{ .updates }}、失敗 {{ .failed }}、スキップ {{ .skipped }}" +ContainerImageCheckFailures: "{{ .count }} コンテナの確認に失敗しました。タスクログを確認してください" + +ErrImageCheckProxyRequired: "現在の実行環境でイメージの更新を確認するには、先にプロキシサーバーを設定してください。" diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index a29e79dcc..b8b95c714 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -802,3 +802,15 @@ ErrFeishuUpgradeMounts: "현재 마운트는 전체 백업 및 복원을 지원 AgentFeishuRecoveryDisabled: "게이트웨이가 복구되었습니다. Feishu는 비활성 상태이며 원본 설정 백업은 유지됩니다." ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" +ContainerImageCheckUpdate: "이미지 업데이트 확인" +ContainerImageCheckScope: "페이지 및 필터와 관계없이 현재 노드의 모든 컨테이너 {{ .count }}개를 확인합니다" +ContainerImageCheckItem: "컨테이너 {{ .name }}, 이미지 {{ .image }} 확인 중" +ContainerImageCheckFailed: "컨테이너 {{ .name }} 확인 실패: {{ .error }}. 현재 이미지와 일치하는 이전 결과를 유지합니다" +ContainerImageCheckSkipped: "{{ .name }} 건너뜀: 태그가 있는 Docker Hub 이미지가 아니거나 로컬 이미지 또는 digest가 고정된 이미지입니다" +ContainerImageCheckAvailable: "컨테이너 {{ .name }} 업데이트 가능: {{ .image }} → {{ .target }}" +ContainerImageCheckCurrent: "컨테이너 {{ .name }}의 업데이트를 찾지 못했습니다" +ContainerImageCheckChanged: "컨테이너 {{ .name }}이 삭제되었거나 이미지가 변경되어 결과를 저장하지 않았습니다" +ContainerImageCheckSummary: "확인 종료: 성공 {{ .checked }}, 업데이트 가능 {{ .updates }}, 실패 {{ .failed }}, 건너뜀 {{ .skipped }}" +ContainerImageCheckFailures: "컨테이너 {{ .count }}개 확인에 실패했습니다. 작업 로그를 확인하세요" + +ErrImageCheckProxyRequired: "현재 실행 환경에서 이미지 업데이트를 확인하려면 먼저 프록시 서버를 설정하세요." diff --git a/agent/i18n/lang/lo.yaml b/agent/i18n/lang/lo.yaml index 1b97892a7..ad9a152fa 100644 --- a/agent/i18n/lang/lo.yaml +++ b/agent/i18n/lang/lo.yaml @@ -793,3 +793,15 @@ ErrFeishuUpgradeMounts: "ຈຸດເຊື່ອມຂໍ້ມູນນີ້ AgentFeishuRecoveryDisabled: "ເກດເວກູ້ຄືນແລ້ວ ແຕ່ Feishu ຍັງປິດ; ສຳຮອງການຕັ້ງຄ່າເດີມຍັງຖືກເກັບໄວ້." ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" +ContainerImageCheckUpdate: "ກວດສອບການອັບເດດອິມເມຈ" +ContainerImageCheckScope: "ກຳລັງກວດສອບທັງໝົດ {{ .count }} ຄອນເທນເນີໃນໂນດນີ້ ໂດຍບໍ່ຂຶ້ນກັບການແບ່ງໜ້າ ຫຼື ຕົວກອງ" +ContainerImageCheckItem: "ກຳລັງກວດສອບຄອນເທນເນີ {{ .name }}, ອິມເມຈ {{ .image }}" +ContainerImageCheckFailed: "ກວດສອບຄອນເທນເນີ {{ .name }} ລົ້ມເຫຼວ: {{ .error }}; ເກັບຜົນກ່ອນໜ້າທີ່ກົງກັບອິມເມຈປັດຈຸບັນໄວ້" +ContainerImageCheckSkipped: "ຂ້າມ {{ .name }}: ບໍ່ແມ່ນອິມເມຈ Docker Hub ທີ່ມີແທັກ, ເປັນອິມເມຈພາຍໃນ ຫຼື ກຳນົດ digest ຄົງທີ່" +ContainerImageCheckAvailable: "ຄອນເທນເນີ {{ .name }} ມີອັບເດດ: {{ .image }} → {{ .target }}" +ContainerImageCheckCurrent: "ບໍ່ພົບອັບເດດສຳລັບຄອນເທນເນີ {{ .name }}" +ContainerImageCheckChanged: "ຄອນເທນເນີ {{ .name }} ຖືກລຶບ ຫຼື ອິມເມຈປ່ຽນແປງ; ບໍ່ໄດ້ບັນທຶກຜົນ" +ContainerImageCheckSummary: "ກວດສອບສິ້ນສຸດ: ກວດແລ້ວ {{ .checked }}, ອັບເກຣດໄດ້ {{ .updates }}, ລົ້ມເຫຼວ {{ .failed }}, ຂ້າມ {{ .skipped }}" +ContainerImageCheckFailures: "ກວດສອບ {{ .count }} ຄອນເທນເນີລົ້ມເຫຼວ; ກະລຸນາເບິ່ງບັນທຶກວຽກ" + +ErrImageCheckProxyRequired: "ເພື່ອກວດສອບການອັບເດດອິມເມຈໃນສະພາບແວດລ້ອມປັດຈຸບັນ ກະລຸນາຕັ້ງຄ່າພຣັອກຊີເຊີບເວີກ່ອນ." diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index 5536c3089..3b7882487 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -802,3 +802,15 @@ ErrFeishuUpgradeMounts: "Lekapan ini tidak boleh disandar dan dipulihkan sepenuh AgentFeishuRecoveryDisabled: "Gerbang dipulihkan dengan Feishu dinyahaktifkan; sandaran konfigurasi asal dikekalkan." ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" +ContainerImageCheckUpdate: "Semak kemas kini imej" +ContainerImageCheckScope: "Menyemak kesemua {{ .count }} kontena pada nod ini tanpa mengira pembahagian halaman atau penapis" +ContainerImageCheckItem: "Menyemak kontena {{ .name }}, imej {{ .image }}" +ContainerImageCheckFailed: "Semakan kontena {{ .name }} gagal: {{ .error }}; keputusan terdahulu yang sepadan dengan imej semasa dikekalkan" +ContainerImageCheckSkipped: "Melangkau {{ .name }}: bukan imej Docker Hub bertag, imej setempat atau digest ditetapkan" +ContainerImageCheckAvailable: "Kontena {{ .name }} mempunyai kemas kini: {{ .image }} → {{ .target }}" +ContainerImageCheckCurrent: "Tiada kemas kini ditemui untuk kontena {{ .name }}" +ContainerImageCheckChanged: "Kontena {{ .name }} telah dipadam atau imejnya berubah; keputusan tidak disimpan" +ContainerImageCheckSummary: "Semakan selesai: disemak {{ .checked }}, boleh dikemas kini {{ .updates }}, gagal {{ .failed }}, dilangkau {{ .skipped }}" +ContainerImageCheckFailures: "Semakan {{ .count }} kontena gagal; lihat log tugas" + +ErrImageCheckProxyRequired: "Untuk menyemak kemas kini imej dalam persekitaran semasa, sila konfigurasikan pelayan proksi terlebih dahulu." diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 78980c76b..178bee47a 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -802,3 +802,15 @@ ErrFeishuUpgradeMounts: "Estas montagens não permitem backup e restauração co AgentFeishuRecoveryDisabled: "Gateway recuperado com Feishu desativado; o backup da configuração original foi mantido." ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" +ContainerImageCheckUpdate: "Verificar atualizações de imagens" +ContainerImageCheckScope: "Verificando todos os {{ .count }} contêineres deste nó, independentemente da paginação e dos filtros" +ContainerImageCheckItem: "Verificando o contêiner {{ .name }}, imagem {{ .image }}" +ContainerImageCheckFailed: "Falha ao verificar o contêiner {{ .name }}: {{ .error }}; os resultados anteriores correspondentes à imagem atual serão mantidos" +ContainerImageCheckSkipped: "Ignorando {{ .name }}: não é uma imagem do Docker Hub com tag, é uma imagem local ou tem digest fixo" +ContainerImageCheckAvailable: "O contêiner {{ .name }} tem uma atualização: {{ .image }} → {{ .target }}" +ContainerImageCheckCurrent: "Nenhuma atualização encontrada para o contêiner {{ .name }}" +ContainerImageCheckChanged: "O contêiner {{ .name }} foi removido ou sua imagem mudou; o resultado não foi salvo" +ContainerImageCheckSummary: "Verificação concluída: verificados {{ .checked }}, atualizáveis {{ .updates }}, falhas {{ .failed }}, ignorados {{ .skipped }}" +ContainerImageCheckFailures: "Falha na verificação de {{ .count }} contêineres; consulte o log da tarefa" + +ErrImageCheckProxyRequired: "Para verificar atualizações de imagens no ambiente atual, configure primeiro um servidor proxy." diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index fe41f9f58..8610affff 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -802,3 +802,15 @@ ErrFeishuUpgradeMounts: "Эти монтирования не поддержив AgentFeishuRecoveryDisabled: "Шлюз восстановлен, Feishu отключён; исходная резервная копия конфигурации сохранена." ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" +ContainerImageCheckUpdate: "Проверить обновления образов" +ContainerImageCheckScope: "Проверка всех контейнеров на текущем узле ({{ .count }}) независимо от страниц и фильтров" +ContainerImageCheckItem: "Проверка контейнера {{ .name }}, образ {{ .image }}" +ContainerImageCheckFailed: "Ошибка проверки контейнера {{ .name }}: {{ .error }}; сохранены предыдущие результаты, соответствующие текущему образу" +ContainerImageCheckSkipped: "Пропуск {{ .name }}: это не образ Docker Hub с тегом, локальный образ или образ с фиксированным digest" +ContainerImageCheckAvailable: "Доступно обновление контейнера {{ .name }}: {{ .image }} → {{ .target }}" +ContainerImageCheckCurrent: "Обновлений для контейнера {{ .name }} не найдено" +ContainerImageCheckChanged: "Контейнер {{ .name }} удалён или его образ изменился; результат не сохранён" +ContainerImageCheckSummary: "Проверка завершена: проверено {{ .checked }}, доступны обновления {{ .updates }}, ошибок {{ .failed }}, пропущено {{ .skipped }}" +ContainerImageCheckFailures: "Не удалось проверить контейнеры: {{ .count }}. Подробности в журнале задачи" + +ErrImageCheckProxyRequired: "Для проверки обновлений образов в текущей среде сначала настройте прокси-сервер." diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index b9122edfa..e93f6d207 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -802,3 +802,15 @@ ErrFeishuUpgradeMounts: "Bu bağlamalar tam yedeklenip geri yüklenemiyor. Uygul AgentFeishuRecoveryDisabled: "Ağ geçidi kurtarıldı, Feishu kapalı; özgün yapılandırma yedeği korundu." ContainerUpgradeWaiting: "Waiting for a container upgrade execution slot" +ContainerImageCheckUpdate: "İmaj güncellemelerini denetle" +ContainerImageCheckScope: "Sayfalama ve filtrelerden bağımsız olarak bu düğümdeki {{ .count }} konteynerin tümü denetleniyor" +ContainerImageCheckItem: "Konteyner {{ .name }}, imaj {{ .image }} denetleniyor" +ContainerImageCheckFailed: "{{ .name }} konteyneri denetlenemedi: {{ .error }}; mevcut imajla eşleşen önceki sonuçlar korunuyor" +ContainerImageCheckSkipped: "{{ .name }} atlanıyor: etiketli bir Docker Hub imajı değil, yerel bir imaj veya digest değeri sabitlenmiş" +ContainerImageCheckAvailable: "{{ .name }} konteyneri için güncelleme mevcut: {{ .image }} → {{ .target }}" +ContainerImageCheckCurrent: "{{ .name }} konteyneri için güncelleme bulunamadı" +ContainerImageCheckChanged: "{{ .name }} konteyneri silindi veya imajı değişti; sonuç kaydedilmedi" +ContainerImageCheckSummary: "Denetim tamamlandı: denetlenen {{ .checked }}, güncellenebilir {{ .updates }}, başarısız {{ .failed }}, atlanan {{ .skipped }}" +ContainerImageCheckFailures: "{{ .count }} konteyner denetlenemedi; görev günlüğünü inceleyin" + +ErrImageCheckProxyRequired: "Geçerli çalışma ortamında imaj güncellemelerini kontrol etmek için önce bir proxy sunucusu yapılandırın." diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 959eb0b3d..1841cdd03 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -802,3 +802,15 @@ ErrFeishuUpgradeMounts: "目前掛載無法完整備份或還原,請使用應 AgentFeishuRecoveryDisabled: "閘道已恢復,飛書保持停用;原設定備份已保留。" ContainerUpgradeWaiting: "等待容器升級任務執行名額" +ContainerImageCheckUpdate: "檢查更新映像" +ContainerImageCheckScope: "檢查目前節點全部 {{ .count }} 個容器,不受分頁和篩選影響" +ContainerImageCheckItem: "檢查容器 {{ .name }},映像 {{ .image }}" +ContainerImageCheckFailed: "容器 {{ .name }} 檢查失敗:{{ .error }};保留符合目前映像的上次結果" +ContainerImageCheckSkipped: "略過 {{ .name }}:非 Docker Hub 標籤映像、本機映像或固定 digest" +ContainerImageCheckAvailable: "容器 {{ .name }} 可升級:{{ .image }} → {{ .target }}" +ContainerImageCheckCurrent: "容器 {{ .name }} 未發現更新" +ContainerImageCheckChanged: "容器 {{ .name }} 已刪除或映像已變更,未儲存本次結果" +ContainerImageCheckSummary: "檢查結束:成功 {{ .checked }},可升級 {{ .updates }},失敗 {{ .failed }},略過 {{ .skipped }}" +ContainerImageCheckFailures: "{{ .count }} 個容器檢查失敗,請查看任務日誌" + +ErrImageCheckProxyRequired: "目前運行環境下,如需檢查映像是否更新,請先設定代理伺服器" diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index c51d41821..8052c5ba9 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -802,3 +802,15 @@ ErrFeishuUpgradeMounts: "当前挂载无法完整备份或恢复,请使用应 AgentFeishuRecoveryDisabled: "网关已恢复,飞书保持停用;原配置备份已保留。" ContainerUpgradeWaiting: "等待容器升级任务执行名额" +ContainerImageCheckUpdate: "检查更新镜像" +ContainerImageCheckScope: "检查当前节点全部 {{ .count }} 个容器,不受分页和筛选影响" +ContainerImageCheckItem: "检查容器 {{ .name }},镜像 {{ .image }}" +ContainerImageCheckFailed: "容器 {{ .name }} 检查失败:{{ .error }};保留匹配当前镜像的上次结果" +ContainerImageCheckSkipped: "跳过 {{ .name }}:非 Docker Hub 标签镜像、本地镜像或固定 digest" +ContainerImageCheckAvailable: "容器 {{ .name }} 可升级:{{ .image }} → {{ .target }}" +ContainerImageCheckCurrent: "容器 {{ .name }} 未发现更新" +ContainerImageCheckChanged: "容器 {{ .name }} 已删除或镜像发生变化,未保存本次结果" +ContainerImageCheckSummary: "检查结束:成功 {{ .checked }},可升级 {{ .updates }},失败 {{ .failed }},跳过 {{ .skipped }}" +ContainerImageCheckFailures: "{{ .count }} 个容器检查失败,请查看任务日志" + +ErrImageCheckProxyRequired: "当前运行环境下,如需检查镜像是否更新,请优先配置代理服务器" diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index b075fe247..69a5587ed 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -119,6 +119,7 @@ func agentDBMigrations() []*gormigrate.Migration { migrations.AddQuickDirectory, migrations.AddLocalModelAccountLink, migrations.AddOllamaModelAccountSync, + migrations.AddContainerImageCheckUpdates, } } diff --git a/agent/init/migration/migrations/container_image_check_update.go b/agent/init/migration/migrations/container_image_check_update.go new file mode 100644 index 000000000..fd16f8d2c --- /dev/null +++ b/agent/init/migration/migrations/container_image_check_update.go @@ -0,0 +1,17 @@ +package migrations + +import ( + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +var AddContainerImageCheckUpdates = &gormigrate.Migration{ + ID: "20261028-container-image-updates", + Migrate: func(tx *gorm.DB) error { + if err := tx.AutoMigrate(&model.ContainerImageCheckUpdate{}); err != nil { + return err + } + return tx.Where("key = ?", "ContainerImageUpdateEnabled").FirstOrCreate(&model.Setting{Key: "ContainerImageUpdateEnabled", Value: "Disable"}).Error + }, +} diff --git a/agent/router/ro_container.go b/agent/router/ro_container.go index 8a715a18c..b8da50cdf 100644 --- a/agent/router/ro_container.go +++ b/agent/router/ro_container.go @@ -16,6 +16,9 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) { baRouter.POST("", baseApi.ContainerCreate) baRouter.POST("/update", baseApi.ContainerUpdate) baRouter.POST("/upgrade", baseApi.ContainerUpgrade) + baRouter.GET("/image-updates", baseApi.GetContainerImageCheckUpdateStatus) + baRouter.POST("/image-updates/check", baseApi.ContainerImageCheckUpdate) + baRouter.POST("/image-updates/schedule", baseApi.SetContainerImageCheckUpdateSchedule) baRouter.POST("/info", baseApi.ContainerInfo) baRouter.POST("/search", baseApi.SearchContainer) baRouter.POST("/list", baseApi.ListContainer) diff --git a/agent/utils/dockerhub/client.go b/agent/utils/dockerhub/client.go new file mode 100644 index 000000000..89c6620e2 --- /dev/null +++ b/agent/utils/dockerhub/client.go @@ -0,0 +1,323 @@ +// Package dockerhub checks Docker Hub metadata without downloading image layers. +package dockerhub + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "regexp" + "sort" + "strconv" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/encrypt" + "github.com/distribution/reference" +) + +const registryURL = "https://registry-1.docker.io" + +// Reference only accepts tagged Docker Hub references. Digest pins are intentional. +func Reference(raw string) (string, string, bool) { + if strings.HasPrefix(raw, "sha256:") { + return "", "", false + } + ref, err := reference.ParseNormalizedNamed(raw) + if err != nil || reference.Domain(ref) != "docker.io" { + return "", "", false + } + if _, pinned := ref.(reference.Digested); pinned { + return "", "", false + } + tagged := reference.TagNameOnly(ref).(reference.NamedTagged) + return reference.Path(ref), tagged.Tag(), true +} + +type Client struct { + HTTP *http.Client + repository string + token string +} + +type Platform struct{ OS, Architecture, Variant string } +type Target struct{ Tag, Digest, ConfigDigest string } + +func New(ctx context.Context, repository, username, password string, httpClient *http.Client) (*Client, error) { + if httpClient == nil { + httpClient = &http.Client{Timeout: 30 * time.Second} + } + c := &Client{HTTP: httpClient, repository: repository} + endpoint := "https://auth.docker.io/token?service=registry.docker.io&scope=" + url.QueryEscape("repository:"+repository+":pull") + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + if username != "" { + req.SetBasicAuth(username, password) + } + resp, err := c.HTTP.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Docker Hub authentication: HTTP %d", resp.StatusCode) + } + var body struct { + Token string `json:"token"` + AccessToken string `json:"access_token"` + } + if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&body); err != nil { + return nil, err + } + c.token = body.Token + if c.token == "" { + c.token = body.AccessToken + } + if c.token == "" { + return nil, fmt.Errorf("Docker Hub returned an empty token") + } + return c, nil +} + +func (c *Client) get(ctx context.Context, endpoint string, result interface{}) (http.Header, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+c.token) + req.Header.Set("Accept", "application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json") + resp, err := c.HTTP.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Docker Hub metadata: HTTP %d", resp.StatusCode) + } + err = json.NewDecoder(io.LimitReader(resp.Body, 16<<20)).Decode(result) + return resp.Header, err +} + +func (c *Client) Tags(ctx context.Context) ([]string, error) { + endpoint := registryURL + "/v2/" + c.repository + "/tags/list?n=1000" + var tags []string + for page := 0; page < 100; page++ { + var body struct { + Tags []string `json:"tags"` + } + headers, err := c.get(ctx, endpoint, &body) + if err != nil { + return nil, err + } + tags = append(tags, body.Tags...) + link := headers.Get("Link") + if link == "" { + return tags, nil + } + start, end := strings.Index(link, "<"), strings.Index(link, ">") + if start < 0 || end <= start { + return nil, fmt.Errorf("invalid Docker Hub pagination") + } + base, _ := url.Parse(registryURL) + next, err := base.Parse(link[start+1 : end]) + if err != nil || next.Scheme != "https" || next.Host != base.Host || next.Path != "/v2/"+c.repository+"/tags/list" { + return nil, fmt.Errorf("invalid Docker Hub pagination URL") + } + endpoint = next.String() + } + return nil, fmt.Errorf("Docker Hub tag pagination limit exceeded") +} + +type manifest struct { + Config struct { + Digest string `json:"digest"` + } `json:"config"` + Manifests []struct { + Digest string `json:"digest"` + Platform struct { + OS string `json:"os"` + Architecture string `json:"architecture"` + Variant string `json:"variant"` + } `json:"platform"` + } `json:"manifests"` +} + +func (c *Client) Resolve(ctx context.Context, tag string, platform Platform) (Target, bool, error) { + var m manifest + headers, err := c.get(ctx, registryURL+"/v2/"+c.repository+"/manifests/"+url.PathEscape(tag), &m) + if err != nil { + return Target{}, false, err + } + digest := headers.Get("Docker-Content-Digest") + if len(m.Manifests) > 0 { + digest = "" + for _, item := range m.Manifests { + if item.Platform.OS == platform.OS && item.Platform.Architecture == platform.Architecture && normalizedVariant(platform.Architecture, item.Platform.Variant) == normalizedVariant(platform.Architecture, platform.Variant) { + digest = item.Digest + break + } + } + if digest == "" { + return Target{}, false, nil + } + m = manifest{} + if _, err := c.get(ctx, registryURL+"/v2/"+c.repository+"/manifests/"+url.PathEscape(digest), &m); err != nil { + return Target{}, false, err + } + } + if m.Config.Digest == "" { + return Target{}, false, fmt.Errorf("unsupported Docker Hub manifest") + } + // Validate single-platform manifests too; the config digest is the Docker Image ID. + var config struct { + OS string `json:"os"` + Architecture string `json:"architecture"` + Variant string `json:"variant"` + } + if _, err := c.get(ctx, registryURL+"/v2/"+c.repository+"/blobs/"+url.PathEscape(m.Config.Digest), &config); err != nil { + return Target{}, false, err + } + if config.OS != platform.OS || config.Architecture != platform.Architecture || (platform.Variant != "" && config.Variant != "" && config.Variant != platform.Variant) { + return Target{}, false, nil + } + return Target{Tag: tag, Digest: digest, ConfigDigest: m.Config.Digest}, true, nil +} + +var versionPattern = regexp.MustCompile(`^v?(\d+)\.(\d+)(?:\.(\d+))?(-[a-zA-Z][a-zA-Z0-9_.-]*)?$`) +var prereleasePattern = regexp.MustCompile(`(?i)(^|[-_.])(alpha|beta|rc|dev|nightly|edge|test|unstable|preview)([0-9_.-]|$)`) + +type version struct { + numbers [3]uint64 + parts int + variant string +} + +func parseVersion(tag string) (version, bool) { + m := versionPattern.FindStringSubmatch(tag) + if m == nil || prereleasePattern.MatchString(m[4]) { + return version{}, false + } + v := version{parts: 2, variant: m[4]} + for i := 0; i < 3; i++ { + if m[i+1] != "" { + n, err := strconv.ParseUint(m[i+1], 10, 64) + if err != nil { + return version{}, false + } + v.numbers[i] = n + } + } + if m[3] != "" { + v.parts = 3 + } + return v, true +} +func greater(a, b version) bool { + for i := 0; i < 3; i++ { + if a.numbers[i] != b.numbers[i] { + return a.numbers[i] > b.numbers[i] + } + } + return false +} + +// Candidates preserves the current tag's major version, variant and precision. Unknown/floating +// tags are checked only for content changes, never guessed from upload dates. +func Candidates(current string, tags []string) []string { + baseline, ok := parseVersion(current) + if !ok { + return []string{current} + } + type candidate struct { + tag string + version version + } + candidates := []candidate{} + seen := map[string]bool{} + for _, tag := range tags { + v, valid := parseVersion(tag) + if valid && v.numbers[0] == baseline.numbers[0] && v.parts == baseline.parts && v.variant == baseline.variant && greater(v, baseline) && !seen[tag] { + candidates = append(candidates, candidate{tag: tag, version: v}) + seen[tag] = true + } + } + sort.Slice(candidates, func(i, j int) bool { + a, b := candidates[i], candidates[j] + if a.version.numbers == b.version.numbers { + return a.tag < b.tag + } + return greater(a.version, b.version) + }) + result := make([]string, 0, len(candidates)+1) + for _, candidate := range candidates { + result = append(result, candidate.tag) + } + + return append(result, current) +} + +func NeedsTags(tag string) bool { _, ok := parseVersion(tag); return ok } + +func normalizedVariant(architecture, variant string) string { + if architecture == "arm64" && variant == "" { + return "v8" + } + return variant +} + +// NewHTTPClient reads the synchronized panel settings for each check, including cron. +func NewHTTPClient() (*http.Client, error) { + var rows []model.Setting + if err := global.DB.Where("key IN ?", []string{"Edition", "ProxyType", "ProxyUrl", "ProxyPort", "ProxyUser", "ProxyPasswd"}).Find(&rows).Error; err != nil { + return nil, err + } + settings := make(map[string]string, len(rows)) + for _, row := range rows { + settings[row.Key] = row.Value + } + client := &http.Client{Timeout: 30 * time.Second} + if settings["Edition"] == "intl" { + return client, nil + } + kind, host, port := settings["ProxyType"], strings.TrimSpace(settings["ProxyUrl"]), settings["ProxyPort"] + if kind == "" || kind == "close" || host == "" || port == "" { + return nil, buserr.New("ErrImageCheckProxyRequired") + } + if kind != "http" && kind != "https" && kind != "socks5" { + return nil, buserr.New("ErrImageCheckProxyRequired") + } + number, err := strconv.Atoi(port) + if err != nil || number < 1 || number > 65535 { + return nil, buserr.New("ErrImageCheckProxyRequired") + } + if !strings.Contains(host, "://") { + host = kind + "://" + host + } + address, err := url.Parse(host) + if err != nil || address.Scheme != kind || address.Hostname() == "" || address.User != nil || address.RawQuery != "" || address.Fragment != "" || (address.Path != "" && address.Path != "/") { + return nil, buserr.New("ErrImageCheckProxyRequired") + } + address.Host = net.JoinHostPort(address.Hostname(), port) + address.Path = "" + if settings["ProxyUser"] != "" { + password, err := encrypt.StringDecrypt(settings["ProxyPasswd"]) + if err != nil { + return nil, buserr.New("ErrImageCheckProxyRequired") + } + address.User = url.UserPassword(settings["ProxyUser"], password) + } + transport := http.DefaultTransport.(*http.Transport).Clone() + // Explicit proxy selection prevents NO_PROXY from bypassing the panel proxy. + // net/http also supports SOCKS5 URLs and proxy authentication. + transport.Proxy = http.ProxyURL(address) + client.Transport = transport + return client, nil +} diff --git a/frontend/src/api/interface/container.ts b/frontend/src/api/interface/container.ts index cb1f65718..5b4cc0086 100644 --- a/frontend/src/api/interface/container.ts +++ b/frontend/src/api/interface/container.ts @@ -136,6 +136,9 @@ export namespace Container { mode: string; } export interface ContainerInfo { + latestVersion?: string; + targetImage?: string; + hasUpdate?: boolean; containerID: string; name: string; imageName: string; diff --git a/frontend/src/api/modules/container.ts b/frontend/src/api/modules/container.ts index 09c15a1a3..3d546d9f7 100644 --- a/frontend/src/api/modules/container.ts +++ b/frontend/src/api/modules/container.ts @@ -290,3 +290,34 @@ export const updateIpv6Option = (fixedCidrV6: string, ip6Tables: boolean, experi export const updateDaemonJsonByfile = (params: Container.DaemonJsonUpdateByFile) => { return http.post(`/containers/daemonjson/update/byfile`, params, TimeoutEnum.T_3M); }; + +export interface ContainerImageCheckUpdateStatus { + proxyRequired: boolean; + taskID: string; + enabled: boolean; + running: boolean; + checked: number; + updates: number; + failed: number; + skipped: number; + error: string; + finishedAt: number; +} +export const getContainerImageCheckUpdateStatus = (node: string, signal?: AbortSignal) => + http.get( + '/containers/image-updates', + {}, + { headers: { CurrentNode: node }, signal }, + ); +export const runContainerImageCheckUpdate = (node: string) => + http.postWithConfig( + '/containers/image-updates/check', + {}, + { headers: { CurrentNode: node } }, + ); +export const setContainerImageCheckUpdateSchedule = (enabled: boolean, node: string) => + http.postWithConfig( + '/containers/image-updates/schedule', + { enabled }, + { headers: { CurrentNode: node } }, + ); diff --git a/frontend/src/components/table/TableOperationActions.vue b/frontend/src/components/table/TableOperationActions.vue index 13eb2de68..25d710ac4 100644 --- a/frontend/src/components/table/TableOperationActions.vue +++ b/frontend/src/components/table/TableOperationActions.vue @@ -13,14 +13,16 @@ @click.stop="handleButtonClick(button)" > - {{ button.label }} + + {{ button.label }} + - {{ t('fu.table.more') }} + {{ t('fu.table.more') }} @@ -75,6 +79,9 @@ const actions = computed(() => { }; }); +const hasDot = (button: FuTableOperationButton) => + typeof button.dot === 'function' ? button.dot(props.row) : Boolean(button.dot); + const handleButtonClick = (button: FuTableOperationButton) => { if (!isOperationDisabled(button, props.row)) { button.click?.(props.row); diff --git a/frontend/src/components/table/shared.ts b/frontend/src/components/table/shared.ts index 2f4f09820..69eac5a1c 100644 --- a/frontend/src/components/table/shared.ts +++ b/frontend/src/components/table/shared.ts @@ -23,6 +23,7 @@ export interface FuTableOperationButton { type?: string; icon?: any; divided?: boolean; + dot?: boolean | ((row: Row) => boolean); } export const FU_TABLE_STORAGE_PREFIX = 'FU-T-'; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 8ad40153c..9c57ef190 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -2116,6 +2116,19 @@ const message = { batchUpgradeWaiting: 'Queued', batchUpgradeRunning: 'Upgrading', batchUpgradeSubmitted: 'Upgrade task submitted', + imageCheckUpdateProxyRequired: + 'To check for image updates in the current runtime environment, please configure a proxy server first.', + imageCheckUpdateConfigureProxy: 'Configure', + imageCheckUpdateAvailableVersion: 'Available version “{version}”', + imageCheckUpdateContentChanged: '(image content changed)', + imageCheckUpdates: 'Check for image updates', + imageCheckUpdateNow: 'Check now', + imageCheckUpdateSchedule: 'Scheduled checks', + imageCheckUpdateDisable: 'Disable scheduled checks', + imageCheckUpdateStarted: 'Checking images used by all containers on this node', + imageCheckUpdateResult: + 'Check complete: {checked} checked, {updates} upgradable, {failed} failed, {skipped} skipped', + 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 1dbfb9f05..250fe6575 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -2135,6 +2135,19 @@ const message = { batchUpgradeWaiting: 'Queued', batchUpgradeRunning: 'Upgrading', batchUpgradeSubmitted: 'Upgrade task submitted', + imageCheckUpdateProxyRequired: + 'Para comprobar actualizaciones de imágenes en el entorno actual, configura primero un servidor proxy.', + imageCheckUpdateConfigureProxy: 'Configurar', + imageCheckUpdateAvailableVersion: 'Versión disponible «{version}»', + imageCheckUpdateContentChanged: '(el contenido de la imagen ha cambiado)', + imageCheckUpdates: 'Buscar actualizaciones de imágenes', + imageCheckUpdateNow: 'Comprobar ahora', + imageCheckUpdateSchedule: 'Comprobación programada', + imageCheckUpdateDisable: 'Desactivar comprobación programada', + imageCheckUpdateStarted: 'Buscando actualizaciones de las imágenes de todos los contenedores de este nodo', + imageCheckUpdateResult: + 'Comprobación finalizada: comprobados {checked}, actualizables {updates}, fallidos {failed}, omitidos {skipped}', + 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 ad8405170..2ee84efd9 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -2093,6 +2093,19 @@ const message = { batchUpgradeWaiting: 'Queued', batchUpgradeRunning: 'Upgrading', batchUpgradeSubmitted: 'Upgrade task submitted', + imageCheckUpdateProxyRequired: + 'برای بررسی به‌روزرسانی تصاویر در محیط فعلی، ابتدا یک سرور پروکسی پیکربندی کنید.', + imageCheckUpdateConfigureProxy: 'پیکربندی', + imageCheckUpdateAvailableVersion: 'نسخه قابل ارتقا «{version}»', + imageCheckUpdateContentChanged: '(محتوای تصویر تغییر کرده است)', + imageCheckUpdates: 'بررسی به‌روزرسانی ایمیج‌ها', + imageCheckUpdateNow: 'بررسی اکنون', + imageCheckUpdateSchedule: 'بررسی زمان‌بندی‌شده', + imageCheckUpdateDisable: 'غیرفعال‌کردن بررسی زمان‌بندی‌شده', + imageCheckUpdateStarted: 'در حال بررسی به‌روزرسانی ایمیج‌های همه کانتینرهای این گره', + imageCheckUpdateResult: + 'بررسی پایان یافت: بررسی‌شده {checked}، قابل ارتقا {updates}، ناموفق {failed}، ردشده {skipped}', + 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 6409a3653..46fc6ec56 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -2100,6 +2100,18 @@ const message = { batchUpgradeWaiting: 'Queued', batchUpgradeRunning: 'Upgrading', batchUpgradeSubmitted: 'Upgrade task submitted', + imageCheckUpdateProxyRequired: + '現在の実行環境でイメージの更新を確認するには、先にプロキシサーバーを設定してください。', + imageCheckUpdateConfigureProxy: '設定へ', + imageCheckUpdateAvailableVersion: '更新可能なバージョン「{version}」', + imageCheckUpdateContentChanged: '(イメージの内容が変更されています)', + imageCheckUpdates: 'イメージの更新を確認', + imageCheckUpdateNow: '今すぐ確認', + imageCheckUpdateSchedule: '定期チェック', + imageCheckUpdateDisable: '定期チェックを無効にする', + imageCheckUpdateStarted: 'このノードのすべてのコンテナで使用されているイメージの更新を確認しています', + imageCheckUpdateResult: '確認完了:成功 {checked}、更新可能 {updates}、失敗 {failed}、スキップ {skipped}', + 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 ed730c915..275b3a48b 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -2075,6 +2075,17 @@ const message = { batchUpgradeWaiting: 'Queued', batchUpgradeRunning: 'Upgrading', batchUpgradeSubmitted: 'Upgrade task submitted', + imageCheckUpdateProxyRequired: '현재 실행 환경에서 이미지 업데이트를 확인하려면 먼저 프록시 서버를 설정하세요.', + imageCheckUpdateConfigureProxy: '설정하기', + imageCheckUpdateAvailableVersion: '업그레이드 가능한 버전 “{version}”', + imageCheckUpdateContentChanged: '(이미지 내용 변경)', + imageCheckUpdates: '이미지 업데이트 확인', + imageCheckUpdateNow: '지금 확인', + imageCheckUpdateSchedule: '정기 확인', + imageCheckUpdateDisable: '정기 확인 끄기', + imageCheckUpdateStarted: '현재 노드의 모든 컨테이너에서 사용하는 이미지의 업데이트를 확인하고 있습니다', + imageCheckUpdateResult: '확인 완료: 성공 {checked}, 업데이트 가능 {updates}, 실패 {failed}, 건너뜀 {skipped}', + 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 18ac51bf2..bfcc66d91 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -2060,6 +2060,19 @@ const message = { batchUpgradeWaiting: 'Queued', batchUpgradeRunning: 'Upgrading', batchUpgradeSubmitted: 'Upgrade task submitted', + imageCheckUpdateProxyRequired: + 'ເພື່ອກວດສອບການອັບເດດອິມເມຈໃນສະພາບແວດລ້ອມປັດຈຸບັນ ກະລຸນາຕັ້ງຄ່າພຣັອກຊີເຊີບເວີກ່ອນ.', + imageCheckUpdateConfigureProxy: 'ໄປຕັ້ງຄ່າ', + imageCheckUpdateAvailableVersion: 'ເວີຊັນທີ່ອັບເກຣດໄດ້ “{version}”', + imageCheckUpdateContentChanged: '(ເນື້ອຫາອິມເມຈປ່ຽນແປງ)', + imageCheckUpdates: 'ກວດສອບການອັບເດດອິມເມຈ', + imageCheckUpdateNow: 'ກວດສອບຕອນນີ້', + imageCheckUpdateSchedule: 'ກວດສອບຕາມຕາຕະລາງ', + imageCheckUpdateDisable: 'ປິດການກວດສອບຕາມຕາຕະລາງ', + imageCheckUpdateStarted: 'ກຳລັງກວດສອບການອັບເດດອິມເມຈຂອງທຸກຄອນເທນເນີໃນໂນດນີ້', + imageCheckUpdateResult: + 'ກວດສອບສຳເລັດ: ກວດແລ້ວ {checked}, ອັບເກຣດໄດ້ {updates}, ລົ້ມເຫຼວ {failed}, ຂ້າມ {skipped}', + 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 726c5f10f..56e6df543 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -2131,6 +2131,19 @@ const message = { batchUpgradeWaiting: 'Queued', batchUpgradeRunning: 'Upgrading', batchUpgradeSubmitted: 'Upgrade task submitted', + imageCheckUpdateProxyRequired: + 'Untuk menyemak kemas kini imej dalam persekitaran semasa, sila konfigurasikan pelayan proksi terlebih dahulu.', + imageCheckUpdateConfigureProxy: 'Konfigurasi', + imageCheckUpdateAvailableVersion: 'Versi tersedia “{version}”', + imageCheckUpdateContentChanged: '(kandungan imej berubah)', + imageCheckUpdates: 'Semak kemas kini imej', + imageCheckUpdateNow: 'Semak sekarang', + imageCheckUpdateSchedule: 'Semakan berjadual', + imageCheckUpdateDisable: 'Matikan semakan berjadual', + imageCheckUpdateStarted: 'Menyemak kemas kini imej yang digunakan oleh semua kontena pada nod ini', + imageCheckUpdateResult: + 'Semakan selesai: disemak {checked}, boleh dikemas kini {updates}, gagal {failed}, dilangkau {skipped}', + 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 c94fe910c..8f0d205c9 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -2134,6 +2134,19 @@ const message = { batchUpgradeWaiting: 'Queued', batchUpgradeRunning: 'Upgrading', batchUpgradeSubmitted: 'Upgrade task submitted', + imageCheckUpdateProxyRequired: + 'Para verificar atualizações de imagens no ambiente atual, configure primeiro um servidor proxy.', + imageCheckUpdateConfigureProxy: 'Configurar', + imageCheckUpdateAvailableVersion: 'Versão disponível “{version}”', + imageCheckUpdateContentChanged: '(conteúdo da imagem alterado)', + imageCheckUpdates: 'Verificar atualizações de imagens', + imageCheckUpdateNow: 'Verificar agora', + imageCheckUpdateSchedule: 'Verificação agendada', + imageCheckUpdateDisable: 'Desativar verificação agendada', + imageCheckUpdateStarted: 'Verificando atualizações das imagens de todos os contêineres deste nó', + imageCheckUpdateResult: + 'Verificação concluída: verificados {checked}, atualizáveis {updates}, falhas {failed}, ignorados {skipped}', + 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 ff486ea02..80a3037f7 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -2127,6 +2127,19 @@ const message = { batchUpgradeWaiting: 'Queued', batchUpgradeRunning: 'Upgrading', batchUpgradeSubmitted: 'Upgrade task submitted', + imageCheckUpdateProxyRequired: + 'Для проверки обновлений образов в текущей среде сначала настройте прокси-сервер.', + imageCheckUpdateConfigureProxy: 'Настроить', + imageCheckUpdateAvailableVersion: 'Доступна версия «{version}»', + imageCheckUpdateContentChanged: '(содержимое образа изменилось)', + imageCheckUpdates: 'Проверить обновления образов', + imageCheckUpdateNow: 'Проверить сейчас', + imageCheckUpdateSchedule: 'Проверка по расписанию', + imageCheckUpdateDisable: 'Отключить проверку по расписанию', + imageCheckUpdateStarted: 'Проверка обновлений образов всех контейнеров на текущем узле', + imageCheckUpdateResult: + 'Проверка завершена: проверено {checked}, доступны обновления {updates}, ошибок {failed}, пропущено {skipped}', + 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 64a1557df..1b08b719a 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -2132,6 +2132,19 @@ const message = { batchUpgradeWaiting: 'Queued', batchUpgradeRunning: 'Upgrading', batchUpgradeSubmitted: 'Upgrade task submitted', + imageCheckUpdateProxyRequired: + 'Geçerli çalışma ortamında imaj güncellemelerini kontrol etmek için önce bir proxy sunucusu yapılandırın.', + imageCheckUpdateConfigureProxy: 'Yapılandır', + imageCheckUpdateAvailableVersion: 'Yükseltilebilir sürüm “{version}”', + imageCheckUpdateContentChanged: '(imaj içeriği değişti)', + imageCheckUpdates: 'İmaj güncellemelerini denetle', + imageCheckUpdateNow: 'Şimdi denetle', + imageCheckUpdateSchedule: 'Zamanlanmış denetim', + imageCheckUpdateDisable: 'Zamanlanmış denetimi kapat', + imageCheckUpdateStarted: 'Bu düğümdeki tüm konteynerlerin imaj güncellemeleri denetleniyor', + imageCheckUpdateResult: + 'Denetim tamamlandı: denetlenen {checked}, güncellenebilir {updates}, başarısız {failed}, atlanan {skipped}', + 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 29b689936..42c323781 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -1997,6 +1997,17 @@ const message = { batchUpgradeWaiting: '等待執行', batchUpgradeRunning: '正在升級', batchUpgradeSubmitted: '已提交升級任務', + imageCheckUpdateProxyRequired: '目前運行環境下,如需檢查映像是否更新,請先設定代理伺服器', + imageCheckUpdateConfigureProxy: '前往設定', + imageCheckUpdateAvailableVersion: '可升級版本「{version}」', + imageCheckUpdateContentChanged: '(映像內容變更)', + imageCheckUpdates: '檢查更新映像', + imageCheckUpdateNow: '立即檢查', + imageCheckUpdateSchedule: '定時檢查', + imageCheckUpdateDisable: '關閉定時檢查', + imageCheckUpdateStarted: '已開始檢查目前節點全部容器的映像更新', + imageCheckUpdateResult: '檢查完成:成功 {checked},可升級 {updates},失敗 {failed},略過 {skipped}', + createByCommand: '指令建立', importContainerBackupTip: '匯入容器備份檔案,僅支援 .tar.gz 格式', importComposeBackupTip: '匯入編排備份檔案,僅支援 .tar.gz 格式', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index eb5db3d94..7bcb8fc88 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -2006,6 +2006,17 @@ const message = { batchUpgradeWaiting: '等待执行', batchUpgradeRunning: '正在升级', batchUpgradeSubmitted: '已提交升级任务', + imageCheckUpdateProxyRequired: '当前运行环境下,如需检查镜像是否更新,请优先配置代理服务器', + imageCheckUpdateConfigureProxy: '去配置', + imageCheckUpdateAvailableVersion: '可升级版本 “{version}”', + imageCheckUpdateContentChanged: '(镜像内容变化)', + imageCheckUpdates: '检查更新镜像', + imageCheckUpdateNow: '立即检查', + imageCheckUpdateSchedule: '定时检查', + imageCheckUpdateDisable: '关闭定时检查', + imageCheckUpdateStarted: '已开始检查当前节点全部容器的镜像更新', + imageCheckUpdateResult: '检查完成:成功 {checked},可升级 {updates},失败 {failed},跳过 {skipped}', + 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 e7049ced7..c1a2112f0 100644 --- a/frontend/src/views/container/container/index.vue +++ b/frontend/src/views/container/container/index.vue @@ -31,6 +31,34 @@ {{ $t('container.containerPrune') }} + + + {{ $t('container.imageCheckUpdates') }} + + + +