mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-30 16:05:30 +00:00
refactor: reuse base settings for image update proxy checks
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ export namespace Setting {
|
||||
complexityVerification: string;
|
||||
noAuthSetting: string;
|
||||
proxyType: string;
|
||||
proxyEnabled: boolean;
|
||||
|
||||
scriptSync: string;
|
||||
dashboardMemoVisible: string;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user