refactor: reuse base settings for image update proxy checks

This commit is contained in:
guoyuqi
2026-10-30 17:42:24 +08:00
parent 8b6328fd04
commit 9644d5429f
12 changed files with 109 additions and 130 deletions
-16
View File
@@ -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)
}
@@ -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
}
-29
View File
@@ -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
}
-1
View File
@@ -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)
-54
View File
@@ -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
}
+68
View File
@@ -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
}
+1
View File
@@ -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"`
+2
View File
@@ -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
}
+1
View File
@@ -97,6 +97,7 @@ export namespace Setting {
complexityVerification: string;
noAuthSetting: string;
proxyType: string;
proxyEnabled: boolean;
scriptSync: string;
dashboardMemoVisible: string;
-10
View File
@@ -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<ImageRemoteCheckStatus>(
'/containers/image/remote/check',
{},
{ headers: { CurrentNode: node }, signal },
);
export interface ContainerImageCheckUpdateStatus {
taskID: string;
enabled: boolean;
+21
View File
@@ -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<Setting.SettingBaseInfo, 'edition' | 'proxyEnabled'>): boolean =>
settings.edition !== 'intl' && !settings.proxyEnabled;
@@ -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 =