From 9644d5429f477da939ea76c27b481ef4608e344b Mon Sep 17 00:00:00 2001 From: guoyuqi Date: Fri, 30 Oct 2026 17:42:24 +0800 Subject: [PATCH] refactor: reuse base settings for image update proxy checks --- agent/app/api/v2/image_remote.go | 16 ----- .../service/container_image_check_update.go | 16 +++-- agent/app/service/image_remote.go | 29 -------- agent/router/ro_container.go | 1 - agent/utils/dockerhub/client.go | 54 --------------- agent/utils/req_helper/request.go | 68 +++++++++++++++++++ core/app/dto/setting.go | 1 + core/app/service/setting.go | 2 + frontend/src/api/interface/setting.ts | 1 + frontend/src/api/modules/container.ts | 10 --- frontend/src/utils/image-remote.ts | 21 ++++++ .../src/views/container/container/index.vue | 20 ++---- 12 files changed, 109 insertions(+), 130 deletions(-) delete mode 100644 agent/app/api/v2/image_remote.go delete mode 100644 agent/app/service/image_remote.go create mode 100644 frontend/src/utils/image-remote.ts diff --git a/agent/app/api/v2/image_remote.go b/agent/app/api/v2/image_remote.go deleted file mode 100644 index 763921907..000000000 --- a/agent/app/api/v2/image_remote.go +++ /dev/null @@ -1,16 +0,0 @@ -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) GetImageRemoteCheckStatus(c *gin.Context) { - status, err := service.GetImageRemoteCheckStatus() - if err != nil { - helper.InternalServer(c, err) - return - } - helper.SuccessWithData(c, status) -} diff --git a/agent/app/service/container_image_check_update.go b/agent/app/service/container_image_check_update.go index bf8347ebb..df4e53085 100644 --- a/agent/app/service/container_image_check_update.go +++ b/agent/app/service/container_image_check_update.go @@ -2,6 +2,7 @@ package service import ( "context" + "errors" "fmt" "io" "net/http" @@ -11,10 +12,12 @@ import ( "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/1Panel-dev/1Panel/agent/utils/req_helper" "github.com/docker/cli/cli/config" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/image" @@ -57,13 +60,13 @@ func GetContainerImageCheckUpdateStatus() (ImageCheckUpdateStatus, error) { func SetContainerImageCheckUpdateSchedule(enabled bool) error { value := "Disable" if enabled { - httpClient, err := dockerhub.NewHTTPClient() + _, err := req_helper.LoadRuntimeProxyURL() + if errors.Is(err, req_helper.ErrProxyRequired) { + return buserr.New("ErrImageCheckProxyRequired") + } 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 @@ -157,7 +160,10 @@ type imageCheckUpdateCache struct { } func runContainerImageCheckUpdate(ctx context.Context, result *ImageCheckUpdateStatus, logger func(string)) error { - httpClient, err := dockerhub.NewHTTPClient() + httpClient, err := req_helper.NewRuntimeHTTPClient() + if errors.Is(err, req_helper.ErrProxyRequired) { + return buserr.New("ErrImageCheckProxyRequired") + } if err != nil { return err } diff --git a/agent/app/service/image_remote.go b/agent/app/service/image_remote.go deleted file mode 100644 index 0f4cc64ae..000000000 --- a/agent/app/service/image_remote.go +++ /dev/null @@ -1,29 +0,0 @@ -package service - -import ( - "errors" - - "github.com/1Panel-dev/1Panel/agent/buserr" - "github.com/1Panel-dev/1Panel/agent/utils/dockerhub" -) - -type ImageRemoteCheckStatus struct { - ProxyRequired bool `json:"proxyRequired"` -} - -// GetImageRemoteCheckStatus validates local settings without contacting Docker Hub. -// Both image searches and update checks use the same proxy policy. -func GetImageRemoteCheckStatus() (ImageRemoteCheckStatus, error) { - client, err := dockerhub.NewHTTPClient() - if err != nil { - var business buserr.BusinessError - if errors.As(err, &business) && business.Msg == "ErrImageCheckProxyRequired" { - return ImageRemoteCheckStatus{ProxyRequired: true}, nil - } - return ImageRemoteCheckStatus{}, err - } - if client.Transport != nil { - client.CloseIdleConnections() - } - return ImageRemoteCheckStatus{}, nil -} diff --git a/agent/router/ro_container.go b/agent/router/ro_container.go index cb351e5aa..b8da50cdf 100644 --- a/agent/router/ro_container.go +++ b/agent/router/ro_container.go @@ -67,7 +67,6 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) { baRouter.POST("/template", baseApi.CreateComposeTemplate) baRouter.POST("/template/del", baseApi.DeleteComposeTemplate) - baRouter.GET("/image/remote/check", baseApi.GetImageRemoteCheckStatus) baRouter.GET("/image", baseApi.ListImage) baRouter.GET("/image/all", baseApi.ListAllImage) baRouter.POST("/image/search", baseApi.SearchImage) diff --git a/agent/utils/dockerhub/client.go b/agent/utils/dockerhub/client.go index 89c6620e2..2d66a49f1 100644 --- a/agent/utils/dockerhub/client.go +++ b/agent/utils/dockerhub/client.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "io" - "net" "net/http" "net/url" "regexp" @@ -15,10 +14,6 @@ import ( "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" ) @@ -272,52 +267,3 @@ func normalizedVariant(architecture, variant string) string { } 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/agent/utils/req_helper/request.go b/agent/utils/req_helper/request.go index 160cea99d..1b489d2b6 100644 --- a/agent/utils/req_helper/request.go +++ b/agent/utils/req_helper/request.go @@ -7,13 +7,17 @@ import ( "io" "net" "net/http" + "net/url" + "strconv" "strings" "time" "github.com/1Panel-dev/1Panel/agent/utils/xpack" + "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" ) func HandleGet(url string) (*http.Response, error) { @@ -162,3 +166,67 @@ func loadRequestTransport() *http.Transport { IdleConnTimeout: 15 * time.Second, } } + +var ErrProxyRequired = errors.New("runtime environment requires a configured proxy") + +// LoadRuntimeProxyURL reads and validates local configuration without creating an HTTP client. +// A nil URL preserves the default network behavior in the global environment. +func LoadRuntimeProxyURL() (*url.URL, 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 + } + if settings["Edition"] == "intl" { + return nil, nil + } + kind, host, port := settings["ProxyType"], strings.TrimSpace(settings["ProxyUrl"]), settings["ProxyPort"] + if kind == "" || kind == "close" || host == "" || port == "" { + return nil, ErrProxyRequired + } + if kind != "http" && kind != "https" && kind != "socks5" { + return nil, ErrProxyRequired + } + number, err := strconv.Atoi(port) + if err != nil || number < 1 || number > 65535 { + return nil, ErrProxyRequired + } + 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, ErrProxyRequired + } + address.Host = net.JoinHostPort(address.Hostname(), port) + address.Path = "" + if settings["ProxyUser"] != "" { + password, err := encrypt.StringDecrypt(settings["ProxyPasswd"]) + if err != nil { + return nil, ErrProxyRequired + } + address.User = url.UserPassword(settings["ProxyUser"], password) + } + return address, nil +} + +// NewRuntimeHTTPClient builds a client for requests using the validated runtime proxy configuration. +func NewRuntimeHTTPClient() (*http.Client, error) { + address, err := LoadRuntimeProxyURL() + if err != nil { + return nil, err + } + client := &http.Client{Timeout: 30 * time.Second} + if address == nil { + return client, nil + } + 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/core/app/dto/setting.go b/core/app/dto/setting.go index 8b9c90a40..eabb57927 100644 --- a/core/app/dto/setting.go +++ b/core/app/dto/setting.go @@ -83,6 +83,7 @@ type SettingBaseInfo struct { ComplexityVerification string `json:"complexityVerification"` NoAuthSetting string `json:"noAuthSetting"` ProxyType string `json:"proxyType"` + ProxyEnabled bool `json:"proxyEnabled"` ScriptSync string `json:"scriptSync"` diff --git a/core/app/service/setting.go b/core/app/service/setting.go index 9b73e67de..bf55e0b01 100644 --- a/core/app/service/setting.go +++ b/core/app/service/setting.go @@ -179,6 +179,8 @@ func (u *SettingService) GetSettingBaseInfo() (*dto.SettingBaseInfo, error) { _ = settingRepo.UpdateOrCreate("MenuAccordion", info.MenuAccordion) } + info.ProxyEnabled = info.ProxyType == "http" || info.ProxyType == "https" || info.ProxyType == "socks5" + return &info, err } diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index e9b6cabbf..49e28a245 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -97,6 +97,7 @@ export namespace Setting { complexityVerification: string; noAuthSetting: string; proxyType: string; + proxyEnabled: boolean; scriptSync: string; dashboardMemoVisible: string; diff --git a/frontend/src/api/modules/container.ts b/frontend/src/api/modules/container.ts index 7e49b763a..4243db9d6 100644 --- a/frontend/src/api/modules/container.ts +++ b/frontend/src/api/modules/container.ts @@ -291,16 +291,6 @@ export const updateDaemonJsonByfile = (params: Container.DaemonJsonUpdateByFile) return http.post(`/containers/daemonjson/update/byfile`, params, TimeoutEnum.T_3M); }; -export interface ImageRemoteCheckStatus { - proxyRequired: boolean; -} -export const getImageRemoteCheckStatus = (node: string, signal?: AbortSignal) => - http.get( - '/containers/image/remote/check', - {}, - { headers: { CurrentNode: node }, signal }, - ); - export interface ContainerImageCheckUpdateStatus { taskID: string; enabled: boolean; diff --git a/frontend/src/utils/image-remote.ts b/frontend/src/utils/image-remote.ts new file mode 100644 index 000000000..ac85a26e6 --- /dev/null +++ b/frontend/src/utils/image-remote.ts @@ -0,0 +1,21 @@ +import type { Setting } from '@/api/interface/setting'; +import { ElMessageBox } from 'element-plus'; +import i18n from '@/lang'; +import { routerToName } from '@/utils/router'; + +export const showImageProxyRequired = async (messageKey: string, isCurrent: () => boolean) => { + try { + await ElMessageBox.confirm(i18n.global.t(messageKey), '', { + confirmButtonText: i18n.global.t('container.imageCheckUpdateConfigureProxy'), + cancelButtonText: i18n.global.t('commons.button.cancel'), + type: 'warning', + }); + if (isCurrent()) routerToName('Panel'); + } catch { + // Cancel leaves the current page unchanged. + } +}; + +// Basic settings expose the environment and proxy status without proxy credentials. +export const isRuntimeProxyRequired = (settings: Pick): boolean => + settings.edition !== 'intl' && !settings.proxyEnabled; diff --git a/frontend/src/views/container/container/index.vue b/frontend/src/views/container/container/index.vue index 0224195f5..fdbe00619 100644 --- a/frontend/src/views/container/container/index.vue +++ b/frontend/src/views/container/container/index.vue @@ -501,11 +501,10 @@ import DockerStatus from '@/views/container/docker-status/index.vue'; import ContainerLogDialog from '@/components/log/container-drawer/index.vue'; import Status from '@/components/status/index.vue'; import { toRef, computed, onMounted, onActivated, onBeforeUnmount, onDeactivated, reactive, ref, watch } from 'vue'; -import { ElMessageBox } from 'element-plus'; +import { showImageProxyRequired, isRuntimeProxyRequired } from '@/utils/image-remote'; import { ArrowDown } from '@element-plus/icons-vue'; import { getContainerImageCheckUpdateStatus, - getImageRemoteCheckStatus, runContainerImageCheckUpdate, setContainerImageCheckUpdateSchedule, type ContainerImageCheckUpdateStatus, @@ -525,7 +524,7 @@ import router from '@/routers'; import { computeSize2 } from '@/utils/size'; import ContainerResource from '@/components/container-resource/index.vue'; import { newUUID } from '@/utils/id'; -import { updateCommonDescription } from '@/api/modules/setting'; +import { updateCommonDescription, getSettingBaseInfo } from '@/api/modules/setting'; const { currentNode, isAdminOrNodeAdmin, isMobile } = useGlobalStore(); @@ -880,19 +879,10 @@ const onImageCheckUpdateCommand = async (command: string) => { imageCheckUpdateStatus.value = status.data; // Disabling an existing schedule must remain possible without a proxy. const proxyStatus = - command === 'check' || !status.data.enabled ? await getImageRemoteCheckStatus(node) : undefined; + node === 'local' && (command === 'check' || !status.data.enabled) ? await getSettingBaseInfo() : undefined; if (generation !== imageCheckUpdateGeneration || node !== currentNode.value) return; - if (proxyStatus?.data.proxyRequired) { - try { - await ElMessageBox.confirm(i18n.global.t('container.imageCheckUpdateProxyRequired'), '', { - confirmButtonText: i18n.global.t('container.imageCheckUpdateConfigureProxy'), - cancelButtonText: i18n.global.t('commons.button.cancel'), - type: 'warning', - }); - if (node === currentNode.value) routerToName('Panel'); - } catch { - // Cancellation leaves the check and schedule untouched. - } + if (proxyStatus && isRuntimeProxyRequired(proxyStatus.data)) { + await showImageProxyRequired('container.imageCheckUpdateProxyRequired', () => node === currentNode.value); return; } const res =