feat: sync httpdns sdk/platform updates without large binaries
This commit is contained in:
@@ -22,7 +22,14 @@
|
||||
"Bash(where protoc:*)",
|
||||
"Bash(/c/Users/robin/AppData/Local/Temp/protoc-install/bin/protoc.exe:*)",
|
||||
"Bash(protoc-gen-go:*)",
|
||||
"Bash(cp:*)"
|
||||
"Bash(cp:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(choco list --local-only)",
|
||||
"Bash(scoop list)",
|
||||
"Bash(go install:*)",
|
||||
"Bash(go env:*)",
|
||||
"Bash(xargs:*)",
|
||||
"Bash(sed:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -5,3 +5,9 @@ deploy/fluent-bit/logs.db
|
||||
deploy/fluent-bit/logs.db-shm
|
||||
deploy/fluent-bit/logs.db-wal
|
||||
deploy/fluent-bit/storage/
|
||||
pkg/
|
||||
.claude
|
||||
|
||||
# Local large build artifacts
|
||||
EdgeAdmin/edge-admin.exe
|
||||
EdgeAPI/deploy/edge-node-linux-amd64-v1.4.9.zip
|
||||
|
||||
@@ -3,5 +3,10 @@
|
||||
# generate 'internal/setup/sql.json' file
|
||||
|
||||
CWD="$(dirname "$0")"
|
||||
SQL_JSON="${CWD}/../internal/setup/sql.json"
|
||||
|
||||
go run "${CWD}"/../cmd/sql-dump/main.go -dir="${CWD}"
|
||||
if [ -f "$SQL_JSON" ]; then
|
||||
echo "sql.json already exists, skipping sql-dump (delete it manually to regenerate)"
|
||||
else
|
||||
go run "${CWD}"/../cmd/sql-dump/main.go -dir="${CWD}"
|
||||
fi
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
Tea.Env = "prod"
|
||||
|
||||
db, err := dbs.Default()
|
||||
if err != nil {
|
||||
fmt.Println("[ERROR]" + err.Error())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package teaconst
|
||||
|
||||
const (
|
||||
Version = "1.4.8" //1.3.9
|
||||
Version = "1.4.9" //1.3.9
|
||||
|
||||
ProductName = "Edge API"
|
||||
ProcessName = "edge-api"
|
||||
@@ -17,6 +17,6 @@ const (
|
||||
|
||||
// 其他节点版本号,用来检测是否有需要升级的节点
|
||||
|
||||
NodeVersion = "1.4.8" //1.3.8.2
|
||||
NodeVersion = "1.4.9" //1.3.8.2
|
||||
)
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
package teaconst
|
||||
|
||||
const (
|
||||
DNSNodeVersion = "1.4.8" //1.3.8.2
|
||||
UserNodeVersion = "1.4.8" //1.3.8.2
|
||||
DNSNodeVersion = "1.4.9" //1.3.8.2
|
||||
UserNodeVersion = "1.4.9" //1.3.8.2
|
||||
ReportNodeVersion = "0.1.5"
|
||||
|
||||
DefaultMaxNodes int32 = 50
|
||||
|
||||
@@ -34,7 +34,7 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
func (this *HTTPDNSClusterDAO) CreateCluster(tx *dbs.Tx, name string, serviceDomain string, defaultTTL int32, fallbackTimeoutMs int32, installDir string, tlsPolicyJSON []byte, isOn bool, isDefault bool, autoRemoteStart bool, accessLogIsOn bool) (int64, error) {
|
||||
func (this *HTTPDNSClusterDAO) CreateCluster(tx *dbs.Tx, name string, serviceDomain string, defaultTTL int32, fallbackTimeoutMs int32, installDir string, tlsPolicyJSON []byte, isOn bool, isDefault bool, autoRemoteStart bool, accessLogIsOn bool, timeZone string) (int64, error) {
|
||||
if isDefault {
|
||||
err := this.Query(tx).
|
||||
State(HTTPDNSClusterStateEnabled).
|
||||
@@ -55,6 +55,7 @@ func (this *HTTPDNSClusterDAO) CreateCluster(tx *dbs.Tx, name string, serviceDom
|
||||
op.IsDefault = isDefault
|
||||
op.AutoRemoteStart = autoRemoteStart
|
||||
op.AccessLogIsOn = accessLogIsOn
|
||||
op.TimeZone = timeZone
|
||||
op.CreatedAt = time.Now().Unix()
|
||||
op.UpdatedAt = time.Now().Unix()
|
||||
op.State = HTTPDNSClusterStateEnabled
|
||||
@@ -68,7 +69,7 @@ func (this *HTTPDNSClusterDAO) CreateCluster(tx *dbs.Tx, name string, serviceDom
|
||||
return types.Int64(op.Id), nil
|
||||
}
|
||||
|
||||
func (this *HTTPDNSClusterDAO) UpdateCluster(tx *dbs.Tx, clusterId int64, name string, serviceDomain string, defaultTTL int32, fallbackTimeoutMs int32, installDir string, tlsPolicyJSON []byte, isOn bool, isDefault bool, autoRemoteStart bool, accessLogIsOn bool) error {
|
||||
func (this *HTTPDNSClusterDAO) UpdateCluster(tx *dbs.Tx, clusterId int64, name string, serviceDomain string, defaultTTL int32, fallbackTimeoutMs int32, installDir string, tlsPolicyJSON []byte, isOn bool, isDefault bool, autoRemoteStart bool, accessLogIsOn bool, timeZone string) error {
|
||||
if isDefault {
|
||||
err := this.Query(tx).
|
||||
State(HTTPDNSClusterStateEnabled).
|
||||
@@ -91,6 +92,7 @@ func (this *HTTPDNSClusterDAO) UpdateCluster(tx *dbs.Tx, clusterId int64, name s
|
||||
op.IsDefault = isDefault
|
||||
op.AutoRemoteStart = autoRemoteStart
|
||||
op.AccessLogIsOn = accessLogIsOn
|
||||
op.TimeZone = timeZone
|
||||
op.UpdatedAt = time.Now().Unix()
|
||||
if len(tlsPolicyJSON) > 0 {
|
||||
op.TLSPolicy = tlsPolicyJSON
|
||||
|
||||
@@ -15,6 +15,7 @@ type HTTPDNSCluster struct {
|
||||
TLSPolicy dbs.JSON `field:"tlsPolicy"` // TLS策略
|
||||
AutoRemoteStart bool `field:"autoRemoteStart"` // 自动远程启动
|
||||
AccessLogIsOn bool `field:"accessLogIsOn"` // 访问日志是否开启
|
||||
TimeZone string `field:"timeZone"` // 时区
|
||||
CreatedAt uint64 `field:"createdAt"` // 创建时间
|
||||
UpdatedAt uint64 `field:"updatedAt"` // 修改时间
|
||||
State uint8 `field:"state"` // 记录状态
|
||||
@@ -32,6 +33,7 @@ type HTTPDNSClusterOperator struct {
|
||||
TLSPolicy any // TLS策略
|
||||
AutoRemoteStart any // 自动远程启动
|
||||
AccessLogIsOn any // 访问日志是否开启
|
||||
TimeZone any // 时区
|
||||
CreatedAt any // 创建时间
|
||||
UpdatedAt any // 修改时间
|
||||
State any // 记录状态
|
||||
|
||||
@@ -2,6 +2,7 @@ package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
@@ -12,8 +13,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
HTTPDNSNodeStateEnabled = 1 // 已启用
|
||||
HTTPDNSNodeStateDisabled = 0 // 已禁用
|
||||
HTTPDNSNodeStateEnabled = 1
|
||||
HTTPDNSNodeStateDisabled = 0
|
||||
)
|
||||
|
||||
type HTTPDNSNodeDAO dbs.DAO
|
||||
@@ -37,7 +38,7 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
// FindEnabledNodeIdWithUniqueId 根据唯一ID获取启用中的HTTPDNS节点ID
|
||||
// FindEnabledNodeIdWithUniqueId 鏍规嵁鍞竴ID鑾峰彇鍚敤涓殑HTTPDNS鑺傜偣ID
|
||||
func (this *HTTPDNSNodeDAO) FindEnabledNodeIdWithUniqueId(tx *dbs.Tx, uniqueId string) (int64, error) {
|
||||
return this.Query(tx).
|
||||
Attr("uniqueId", uniqueId).
|
||||
@@ -46,7 +47,7 @@ func (this *HTTPDNSNodeDAO) FindEnabledNodeIdWithUniqueId(tx *dbs.Tx, uniqueId s
|
||||
FindInt64Col(0)
|
||||
}
|
||||
|
||||
// CreateNode 创建节点
|
||||
// CreateNode 鍒涘缓鑺傜偣
|
||||
func (this *HTTPDNSNodeDAO) CreateNode(tx *dbs.Tx, clusterId int64, name string, installDir string, isOn bool) (int64, error) {
|
||||
uniqueId := rands.HexString(32)
|
||||
secret := rands.String(32)
|
||||
@@ -75,7 +76,7 @@ func (this *HTTPDNSNodeDAO) CreateNode(tx *dbs.Tx, clusterId int64, name string,
|
||||
return types.Int64(op.Id), nil
|
||||
}
|
||||
|
||||
// UpdateNode 更新节点
|
||||
// UpdateNode 鏇存柊鑺傜偣
|
||||
func (this *HTTPDNSNodeDAO) UpdateNode(tx *dbs.Tx, nodeId int64, name string, installDir string, isOn bool) error {
|
||||
var op = NewHTTPDNSNodeOperator()
|
||||
op.Id = nodeId
|
||||
@@ -86,7 +87,7 @@ func (this *HTTPDNSNodeDAO) UpdateNode(tx *dbs.Tx, nodeId int64, name string, in
|
||||
return this.Save(tx, op)
|
||||
}
|
||||
|
||||
// DisableNode 禁用节点
|
||||
// DisableNode 绂佺敤鑺傜偣
|
||||
func (this *HTTPDNSNodeDAO) DisableNode(tx *dbs.Tx, nodeId int64) error {
|
||||
node, err := this.FindEnabledNode(tx, nodeId)
|
||||
if err != nil {
|
||||
@@ -112,7 +113,7 @@ func (this *HTTPDNSNodeDAO) DisableNode(tx *dbs.Tx, nodeId int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// FindEnabledNode 查找启用节点
|
||||
// FindEnabledNode 鏌ユ壘鍚敤鑺傜偣
|
||||
func (this *HTTPDNSNodeDAO) FindEnabledNode(tx *dbs.Tx, nodeId int64) (*HTTPDNSNode, error) {
|
||||
one, err := this.Query(tx).
|
||||
Pk(nodeId).
|
||||
@@ -124,7 +125,7 @@ func (this *HTTPDNSNodeDAO) FindEnabledNode(tx *dbs.Tx, nodeId int64) (*HTTPDNSN
|
||||
return one.(*HTTPDNSNode), nil
|
||||
}
|
||||
|
||||
// FindNodeClusterId 查询节点所属集群ID
|
||||
// FindNodeClusterId 鏌ヨ鑺傜偣鎵€灞為泦缇D
|
||||
func (this *HTTPDNSNodeDAO) FindNodeClusterId(tx *dbs.Tx, nodeId int64) (int64, error) {
|
||||
return this.Query(tx).
|
||||
Pk(nodeId).
|
||||
@@ -133,7 +134,7 @@ func (this *HTTPDNSNodeDAO) FindNodeClusterId(tx *dbs.Tx, nodeId int64) (int64,
|
||||
FindInt64Col(0)
|
||||
}
|
||||
|
||||
// ListEnabledNodes 列出节点
|
||||
// ListEnabledNodes 鍒楀嚭鑺傜偣
|
||||
func (this *HTTPDNSNodeDAO) ListEnabledNodes(tx *dbs.Tx, clusterId int64) (result []*HTTPDNSNode, err error) {
|
||||
query := this.Query(tx).
|
||||
State(HTTPDNSNodeStateEnabled).
|
||||
@@ -145,6 +146,20 @@ func (this *HTTPDNSNodeDAO) ListEnabledNodes(tx *dbs.Tx, clusterId int64) (resul
|
||||
return
|
||||
}
|
||||
|
||||
// FindAllInactiveNodesWithClusterId 取得一个集群离线的HTTPDNS节点
|
||||
func (this *HTTPDNSNodeDAO) FindAllInactiveNodesWithClusterId(tx *dbs.Tx, clusterId int64) (result []*HTTPDNSNode, err error) {
|
||||
_, err = this.Query(tx).
|
||||
State(HTTPDNSNodeStateEnabled).
|
||||
Attr("clusterId", clusterId).
|
||||
Attr("isOn", true). // 只监控启用的节点
|
||||
Attr("isInstalled", true). // 只监控已经安装的节点
|
||||
Attr("isActive", false). // 当前处于离线状态
|
||||
Result("id", "name").
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateNodeStatus 更新节点状态
|
||||
func (this *HTTPDNSNodeDAO) UpdateNodeStatus(tx *dbs.Tx, nodeId int64, isUp bool, isInstalled bool, isActive bool, statusJSON []byte, installStatusJSON []byte) error {
|
||||
var op = NewHTTPDNSNodeOperator()
|
||||
@@ -261,7 +276,46 @@ func (this *HTTPDNSNodeDAO) FindNodeInstallStatus(tx *dbs.Tx, nodeId int64) (*No
|
||||
return installStatus, nil
|
||||
}
|
||||
|
||||
// UpdateNodeIsInstalled 更新节点安装状态位
|
||||
// CountAllLowerVersionNodesWithClusterId 璁$畻鍗曚釜闆嗙兢涓墍鏈変綆浜庢煇涓増鏈殑鑺傜偣鏁伴噺
|
||||
func (this *HTTPDNSNodeDAO) CountAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterId int64, os string, arch string, version string) (int64, error) {
|
||||
return this.Query(tx).
|
||||
State(HTTPDNSNodeStateEnabled).
|
||||
Attr("clusterId", clusterId).
|
||||
Attr("isOn", true).
|
||||
Attr("isUp", true).
|
||||
Attr("isActive", true).
|
||||
Where("status IS NOT NULL").
|
||||
Where("JSON_EXTRACT(status, '$.os')=:os").
|
||||
Where("JSON_EXTRACT(status, '$.arch')=:arch").
|
||||
Where("(JSON_EXTRACT(status, '$.buildVersionCode') IS NULL OR JSON_EXTRACT(status, '$.buildVersionCode')<:version)").
|
||||
Param("os", os).
|
||||
Param("arch", arch).
|
||||
Param("version", utils.VersionToLong(version)).
|
||||
Count()
|
||||
}
|
||||
|
||||
// FindAllLowerVersionNodesWithClusterId 鏌ユ壘鍗曚釜闆嗙兢涓墍鏈変綆浜庢煇涓増鏈殑鑺傜偣
|
||||
func (this *HTTPDNSNodeDAO) FindAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterId int64, os string, arch string, version string) (result []*HTTPDNSNode, err error) {
|
||||
_, err = this.Query(tx).
|
||||
State(HTTPDNSNodeStateEnabled).
|
||||
Attr("clusterId", clusterId).
|
||||
Attr("isOn", true).
|
||||
Attr("isUp", true).
|
||||
Attr("isActive", true).
|
||||
Where("status IS NOT NULL").
|
||||
Where("JSON_EXTRACT(status, '$.os')=:os").
|
||||
Where("JSON_EXTRACT(status, '$.arch')=:arch").
|
||||
Where("(JSON_EXTRACT(status, '$.buildVersionCode') IS NULL OR JSON_EXTRACT(status, '$.buildVersionCode')<:version)").
|
||||
Param("os", os).
|
||||
Param("arch", arch).
|
||||
Param("version", utils.VersionToLong(version)).
|
||||
DescPk().
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateNodeIsInstalled 鏇存柊鑺傜偣瀹夎鐘舵€佷綅
|
||||
func (this *HTTPDNSNodeDAO) UpdateNodeIsInstalled(tx *dbs.Tx, nodeId int64, isInstalled bool) error {
|
||||
_, err := this.Query(tx).
|
||||
Pk(nodeId).
|
||||
|
||||
@@ -1521,6 +1521,8 @@ func (this *NodeDAO) CountAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterI
|
||||
return this.Query(tx).
|
||||
State(NodeStateEnabled).
|
||||
Attr("isOn", true).
|
||||
Attr("isUp", true).
|
||||
Attr("isActive", true).
|
||||
Attr("clusterId", clusterId).
|
||||
Where("status IS NOT NULL").
|
||||
Where("JSON_EXTRACT(status, '$.os')=:os").
|
||||
@@ -1536,6 +1538,9 @@ func (this *NodeDAO) CountAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterI
|
||||
func (this *NodeDAO) FindAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterId int64, os string, arch string, version string) (result []*Node, err error) {
|
||||
_, err = this.Query(tx).
|
||||
State(NodeStateEnabled).
|
||||
Attr("isOn", true).
|
||||
Attr("isUp", true).
|
||||
Attr("isActive", true).
|
||||
Attr("clusterId", clusterId).
|
||||
Where("status IS NOT NULL").
|
||||
Where("JSON_EXTRACT(status, '$.os')=:os").
|
||||
|
||||
@@ -94,6 +94,8 @@ func (this *NSNodeDAO) CountAllLowerVersionNodesWithClusterId(tx *dbs.Tx, cluste
|
||||
State(NSNodeStateEnabled).
|
||||
Attr("clusterId", clusterId).
|
||||
Attr("isOn", true).
|
||||
Attr("isUp", true).
|
||||
Attr("isActive", true).
|
||||
Where("status IS NOT NULL").
|
||||
Where("JSON_EXTRACT(status, '$.os')=:os").
|
||||
Where("JSON_EXTRACT(status, '$.arch')=:arch").
|
||||
@@ -104,6 +106,27 @@ func (this *NSNodeDAO) CountAllLowerVersionNodesWithClusterId(tx *dbs.Tx, cluste
|
||||
Count()
|
||||
}
|
||||
|
||||
// FindAllLowerVersionNodesWithClusterId 查找单个集群中所有低于某个版本的节点
|
||||
func (this *NSNodeDAO) FindAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterId int64, os string, arch string, version string) (result []*NSNode, err error) {
|
||||
_, err = this.Query(tx).
|
||||
State(NSNodeStateEnabled).
|
||||
Attr("clusterId", clusterId).
|
||||
Attr("isOn", true).
|
||||
Attr("isUp", true).
|
||||
Attr("isActive", true).
|
||||
Where("status IS NOT NULL").
|
||||
Where("JSON_EXTRACT(status, '$.os')=:os").
|
||||
Where("JSON_EXTRACT(status, '$.arch')=:arch").
|
||||
Where("(JSON_EXTRACT(status, '$.buildVersionCode') IS NULL OR JSON_EXTRACT(status, '$.buildVersionCode')<:version)").
|
||||
Param("os", os).
|
||||
Param("arch", arch).
|
||||
Param("version", utils.VersionToLong(version)).
|
||||
DescPk().
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// FindEnabledNodeIdWithUniqueId 根据唯一ID获取节点ID
|
||||
func (this *NSNodeDAO) FindEnabledNodeIdWithUniqueId(tx *dbs.Tx, uniqueId string) (int64, error) {
|
||||
return this.Query(tx).
|
||||
|
||||
@@ -209,6 +209,8 @@ func (this *NSNodeDAO) CountAllLowerVersionNodesWithClusterId(tx *dbs.Tx, cluste
|
||||
return this.Query(tx).
|
||||
State(NSNodeStateEnabled).
|
||||
Attr("isOn", true).
|
||||
Attr("isUp", true).
|
||||
Attr("isActive", true).
|
||||
Attr("clusterId", clusterId).
|
||||
Where("status IS NOT NULL").
|
||||
Where("JSON_EXTRACT(status, '$.os')=:os").
|
||||
@@ -412,6 +414,27 @@ func (this *NSNodeDAO) CountAllLowerVersionNodes(tx *dbs.Tx, version string) (in
|
||||
Count()
|
||||
}
|
||||
|
||||
// FindAllLowerVersionNodesWithClusterId 查找单个集群中所有低于某个版本的节点
|
||||
func (this *NSNodeDAO) FindAllLowerVersionNodesWithClusterId(tx *dbs.Tx, clusterId int64, os string, arch string, version string) (result []*NSNode, err error) {
|
||||
_, err = this.Query(tx).
|
||||
State(NSNodeStateEnabled).
|
||||
Attr("clusterId", clusterId).
|
||||
Attr("isOn", true).
|
||||
Attr("isUp", true).
|
||||
Attr("isActive", true).
|
||||
Where("status IS NOT NULL").
|
||||
Where("JSON_EXTRACT(status, '$.os')=:os").
|
||||
Where("JSON_EXTRACT(status, '$.arch')=:arch").
|
||||
Where("(JSON_EXTRACT(status, '$.buildVersionCode') IS NULL OR JSON_EXTRACT(status, '$.buildVersionCode')<:version)").
|
||||
Param("os", os).
|
||||
Param("arch", arch).
|
||||
Param("version", utils.VersionToLong(version)).
|
||||
DescPk().
|
||||
Slice(&result).
|
||||
FindAll()
|
||||
return
|
||||
}
|
||||
|
||||
// ComposeNodeConfig 组合节点配置
|
||||
func (this *NSNodeDAO) ComposeNodeConfig(tx *dbs.Tx, nodeId int64) (*dnsconfigs.NSNodeConfig, error) {
|
||||
if nodeId <= 0 {
|
||||
|
||||
@@ -187,6 +187,100 @@ func (q *HTTPDNSNodeQueue) InstallNode(nodeId int64, installStatus *models.NodeI
|
||||
return installer.Install(installDir, params, installStatus)
|
||||
}
|
||||
|
||||
// StartNode 启动HTTPDNS节点
|
||||
func (q *HTTPDNSNodeQueue) StartNode(nodeId int64) error {
|
||||
node, err := models.SharedHTTPDNSNodeDAO.FindEnabledNode(nil, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if node == nil {
|
||||
return errors.New("can not find node, ID '" + numberutils.FormatInt64(nodeId) + "'")
|
||||
}
|
||||
|
||||
// 登录信息
|
||||
login, err := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(nil, nodeconfigs.NodeRoleHTTPDNS, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if login == nil {
|
||||
return newGrantError("can not find node login information")
|
||||
}
|
||||
loginParams, err := login.DecodeSSHParams()
|
||||
if err != nil {
|
||||
return newGrantError(err.Error())
|
||||
}
|
||||
if len(strings.TrimSpace(loginParams.Host)) == 0 {
|
||||
return newGrantError("ssh host should not be empty")
|
||||
}
|
||||
if loginParams.Port <= 0 {
|
||||
loginParams.Port = 22
|
||||
}
|
||||
if loginParams.GrantId <= 0 {
|
||||
return newGrantError("can not find node grant")
|
||||
}
|
||||
|
||||
grant, err := models.SharedNodeGrantDAO.FindEnabledNodeGrant(nil, loginParams.GrantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if grant == nil {
|
||||
return newGrantError("can not find user grant with id '" + numberutils.FormatInt64(loginParams.GrantId) + "'")
|
||||
}
|
||||
|
||||
installer := &HTTPDNSNodeInstaller{}
|
||||
err = installer.Login(&Credentials{
|
||||
Host: strings.TrimSpace(loginParams.Host),
|
||||
Port: loginParams.Port,
|
||||
Username: grant.Username,
|
||||
Password: grant.Password,
|
||||
PrivateKey: grant.PrivateKey,
|
||||
Passphrase: grant.Passphrase,
|
||||
Method: grant.Method,
|
||||
Sudo: grant.Su == 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = installer.Close()
|
||||
}()
|
||||
|
||||
installDir := strings.TrimSpace(node.InstallDir)
|
||||
if len(installDir) == 0 {
|
||||
cluster, err := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(nil, int64(node.ClusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cluster == nil {
|
||||
return errors.New("can not find cluster, ID '" + numberutils.FormatInt64(int64(node.ClusterId)) + "'")
|
||||
}
|
||||
installDir = strings.TrimSpace(cluster.InstallDir)
|
||||
if len(installDir) == 0 {
|
||||
installDir = installer.client.UserHome() + "/edge-httpdns"
|
||||
}
|
||||
}
|
||||
_, appDir := resolveHTTPDNSInstallPaths(installDir)
|
||||
exeFile := appDir + "/bin/edge-httpdns"
|
||||
|
||||
_, err = installer.client.Stat(exeFile)
|
||||
if err != nil {
|
||||
return errors.New("httpdns node is not installed correctly, can not find executable file: " + exeFile)
|
||||
}
|
||||
|
||||
// 先尝试 systemd 拉起
|
||||
_, _, _ = installer.client.Exec("/usr/bin/systemctl start edge-httpdns")
|
||||
|
||||
_, stderr, err := installer.client.Exec(exeFile + " start")
|
||||
if err != nil {
|
||||
return fmt.Errorf("start failed: %w", err)
|
||||
}
|
||||
if len(strings.TrimSpace(stderr)) > 0 {
|
||||
return errors.New("start failed: " + strings.TrimSpace(stderr))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *HTTPDNSNodeQueue) resolveClusterTLSCertPair(cluster *models.HTTPDNSCluster) ([]byte, []byte, error) {
|
||||
if cluster == nil {
|
||||
return nil, nil, errors.New("cluster not found")
|
||||
|
||||
25
EdgeAPI/internal/installers/upgrade_queue.go
Normal file
25
EdgeAPI/internal/installers/upgrade_queue.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package installers
|
||||
|
||||
// UpgradeQueue 升级队列,控制并发数
|
||||
type UpgradeQueue struct {
|
||||
sem chan struct{}
|
||||
}
|
||||
|
||||
// SharedUpgradeQueue 全局升级队列,最多5个并发
|
||||
var SharedUpgradeQueue = NewUpgradeQueue(5)
|
||||
|
||||
// NewUpgradeQueue 创建升级队列
|
||||
func NewUpgradeQueue(maxConcurrent int) *UpgradeQueue {
|
||||
return &UpgradeQueue{
|
||||
sem: make(chan struct{}, maxConcurrent),
|
||||
}
|
||||
}
|
||||
|
||||
// SubmitNodeUpgrade 提交节点升级任务(异步执行,超过并发限制自动排队)
|
||||
func (q *UpgradeQueue) SubmitNodeUpgrade(nodeId int64, upgradeFunc func(int64) error) {
|
||||
go func() {
|
||||
q.sem <- struct{}{}
|
||||
defer func() { <-q.sem }()
|
||||
_ = upgradeFunc(nodeId)
|
||||
}()
|
||||
}
|
||||
@@ -2,9 +2,12 @@ package httpdns
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
)
|
||||
|
||||
func toPBCluster(cluster *models.HTTPDNSCluster) *pb.HTTPDNSCluster {
|
||||
@@ -25,9 +28,94 @@ func toPBCluster(cluster *models.HTTPDNSCluster) *pb.HTTPDNSCluster {
|
||||
UpdatedAt: int64(cluster.UpdatedAt),
|
||||
AutoRemoteStart: cluster.AutoRemoteStart,
|
||||
AccessLogIsOn: cluster.AccessLogIsOn,
|
||||
TimeZone: cluster.TimeZone,
|
||||
}
|
||||
}
|
||||
|
||||
// toPBClusterWithResolvedCerts 转换集群并解析证书引用为实际 PEM 数据
|
||||
// 供节点调用的 RPC 使用,确保节点能拿到完整的证书内容
|
||||
func toPBClusterWithResolvedCerts(tx *dbs.Tx, cluster *models.HTTPDNSCluster) *pb.HTTPDNSCluster {
|
||||
pbCluster := toPBCluster(cluster)
|
||||
if pbCluster == nil {
|
||||
return nil
|
||||
}
|
||||
resolved := resolveTLSPolicyCerts(tx, cluster.TLSPolicy)
|
||||
if resolved != nil {
|
||||
pbCluster.TlsPolicyJSON = resolved
|
||||
}
|
||||
return pbCluster
|
||||
}
|
||||
|
||||
// resolveTLSPolicyCerts 将 tlsPolicyJSON 中的 certRefs 解析为带实际 PEM 数据的 certs
|
||||
func resolveTLSPolicyCerts(tx *dbs.Tx, tlsPolicyJSON []byte) []byte {
|
||||
if len(tlsPolicyJSON) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 解析外层结构: {"listen": [...], "sslPolicy": {...}}
|
||||
var tlsConfig map[string]json.RawMessage
|
||||
if err := json.Unmarshal(tlsPolicyJSON, &tlsConfig); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
sslPolicyData, ok := tlsConfig["sslPolicy"]
|
||||
if !ok || len(sslPolicyData) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var sslPolicy sslconfigs.SSLPolicy
|
||||
if err := json.Unmarshal(sslPolicyData, &sslPolicy); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查 certs 是否已经有实际数据
|
||||
for _, cert := range sslPolicy.Certs {
|
||||
if cert != nil && len(cert.CertData) > 128 && len(cert.KeyData) > 128 {
|
||||
return nil // 已有完整 PEM 数据,无需处理
|
||||
}
|
||||
}
|
||||
|
||||
// 从 certRefs 解析实际证书数据
|
||||
if len(sslPolicy.CertRefs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var resolvedCerts []*sslconfigs.SSLCertConfig
|
||||
for _, ref := range sslPolicy.CertRefs {
|
||||
if ref == nil || ref.CertId <= 0 {
|
||||
continue
|
||||
}
|
||||
certConfig, err := models.SharedSSLCertDAO.ComposeCertConfig(tx, ref.CertId, false, nil, nil)
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS]resolve cert", ref.CertId, "failed:", err.Error())
|
||||
continue
|
||||
}
|
||||
if certConfig == nil || len(certConfig.CertData) == 0 || len(certConfig.KeyData) == 0 {
|
||||
continue
|
||||
}
|
||||
resolvedCerts = append(resolvedCerts, certConfig)
|
||||
}
|
||||
|
||||
if len(resolvedCerts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 把解析后的证书写回 sslPolicy.Certs
|
||||
sslPolicy.Certs = resolvedCerts
|
||||
|
||||
newPolicyData, err := json.Marshal(&sslPolicy)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
tlsConfig["sslPolicy"] = newPolicyData
|
||||
|
||||
result, err := json.Marshal(tlsConfig)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func toPBNode(node *models.HTTPDNSNode) *pb.HTTPDNSNode {
|
||||
if node == nil {
|
||||
return nil
|
||||
|
||||
@@ -67,10 +67,10 @@ func (this *HTTPDNSAppService) CreateHTTPDNSApp(ctx context.Context, req *pb.Cre
|
||||
return errors.New("appId already exists")
|
||||
}
|
||||
|
||||
// 使用 clusterIdsJSON;若为空则优先从用户关联集群获取,再 fallback 到全局默认
|
||||
// 使用 clusterIdsJSON;若为空则从用户关联集群获取
|
||||
clusterIdsJSON := req.ClusterIdsJSON
|
||||
if len(clusterIdsJSON) == 0 || string(clusterIdsJSON) == "[]" || string(clusterIdsJSON) == "null" {
|
||||
// 优先读取用户关联的 HTTPDNS 集群
|
||||
// 读取用户关联的 HTTPDNS 集群
|
||||
if req.UserId > 0 {
|
||||
user, userErr := models.SharedUserDAO.FindEnabledUser(tx, req.UserId, nil)
|
||||
if userErr != nil {
|
||||
@@ -83,16 +83,11 @@ func (this *HTTPDNSAppService) CreateHTTPDNSApp(ctx context.Context, req *pb.Cre
|
||||
}
|
||||
}
|
||||
}
|
||||
// fallback 到全局默认
|
||||
if len(clusterIdsJSON) == 0 || string(clusterIdsJSON) == "[]" || string(clusterIdsJSON) == "null" {
|
||||
defaultClusterIds, defaultErr := readHTTPDNSDefaultClusterIdList(tx)
|
||||
if defaultErr != nil {
|
||||
return defaultErr
|
||||
}
|
||||
if len(defaultClusterIds) > 0 {
|
||||
clusterIdsJSON, _ = json.Marshal(defaultClusterIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果仍然没有集群,则不允许创建
|
||||
if len(clusterIdsJSON) == 0 || string(clusterIdsJSON) == "[]" || string(clusterIdsJSON) == "null" {
|
||||
return errors.New("用户尚未分配 HTTPDNS 集群,无法创建应用")
|
||||
}
|
||||
|
||||
appDbId, err = models.SharedHTTPDNSAppDAO.CreateApp(tx, appName, appId, clusterIdsJSON, req.IsOn, req.UserId)
|
||||
@@ -143,14 +138,6 @@ func readHTTPDNSDefaultClusterIdList(tx *dbs.Tx) ([]int64, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// fallback:默认主集群
|
||||
primaryClusterId, err := models.SharedHTTPDNSClusterDAO.FindDefaultPrimaryClusterId(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if primaryClusterId > 0 {
|
||||
return []int64{primaryClusterId}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,14 @@ package httpdns
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// HTTPDNSClusterService HTTPDNS集群服务
|
||||
@@ -25,7 +29,7 @@ func (this *HTTPDNSClusterService) CreateHTTPDNSCluster(ctx context.Context, req
|
||||
}
|
||||
var clusterId int64
|
||||
err = this.RunTx(func(tx *dbs.Tx) error {
|
||||
clusterId, err = models.SharedHTTPDNSClusterDAO.CreateCluster(tx, req.Name, req.ServiceDomain, req.DefaultTTL, req.FallbackTimeoutMs, req.InstallDir, req.TlsPolicyJSON, req.IsOn, req.IsDefault, req.AutoRemoteStart, req.AccessLogIsOn)
|
||||
clusterId, err = models.SharedHTTPDNSClusterDAO.CreateCluster(tx, req.Name, req.ServiceDomain, req.DefaultTTL, req.FallbackTimeoutMs, req.InstallDir, req.TlsPolicyJSON, req.IsOn, req.IsDefault, req.AutoRemoteStart, req.AccessLogIsOn, req.TimeZone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -42,13 +46,41 @@ func (this *HTTPDNSClusterService) UpdateHTTPDNSCluster(ctx context.Context, req
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Compatibility fallback:
|
||||
// If protobuf schemas between edge-admin and edge-api are inconsistent,
|
||||
// these newly-added fields may be lost on the wire. Read gRPC metadata as fallback.
|
||||
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
||||
if values := md.Get("x-httpdns-auto-remote-start"); len(values) > 0 {
|
||||
raw := strings.ToLower(strings.TrimSpace(values[0]))
|
||||
req.AutoRemoteStart = raw == "1" || raw == "true" || raw == "on" || raw == "yes" || raw == "enabled"
|
||||
}
|
||||
if values := md.Get("x-httpdns-access-log-is-on"); len(values) > 0 {
|
||||
raw := strings.ToLower(strings.TrimSpace(values[0]))
|
||||
req.AccessLogIsOn = raw == "1" || raw == "true" || raw == "on" || raw == "yes" || raw == "enabled"
|
||||
}
|
||||
if values := md.Get("x-httpdns-time-zone"); len(values) > 0 {
|
||||
raw := strings.TrimSpace(values[0])
|
||||
if len(raw) > 0 {
|
||||
req.TimeZone = raw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = this.RunTx(func(tx *dbs.Tx) error {
|
||||
err = models.SharedHTTPDNSClusterDAO.UpdateCluster(tx, req.ClusterId, req.Name, req.ServiceDomain, req.DefaultTTL, req.FallbackTimeoutMs, req.InstallDir, req.TlsPolicyJSON, req.IsOn, req.IsDefault, req.AutoRemoteStart, req.AccessLogIsOn)
|
||||
// 先读取旧的 TLS 配置,用于判断是否真正发生了变化
|
||||
var oldTLSJSON string
|
||||
oldCluster, findErr := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(tx, req.ClusterId)
|
||||
if findErr == nil && oldCluster != nil {
|
||||
oldTLSJSON = string(oldCluster.TLSPolicy)
|
||||
}
|
||||
|
||||
err = models.SharedHTTPDNSClusterDAO.UpdateCluster(tx, req.ClusterId, req.Name, req.ServiceDomain, req.DefaultTTL, req.FallbackTimeoutMs, req.InstallDir, req.TlsPolicyJSON, req.IsOn, req.IsDefault, req.AutoRemoteStart, req.AccessLogIsOn, req.TimeZone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
taskType := models.HTTPDNSNodeTaskTypeConfigChanged
|
||||
if len(req.TlsPolicyJSON) > 0 {
|
||||
if len(req.TlsPolicyJSON) > 0 && string(req.TlsPolicyJSON) != oldTLSJSON {
|
||||
taskType = models.HTTPDNSNodeTaskTypeTLSChanged
|
||||
}
|
||||
return notifyHTTPDNSClusterTask(tx, req.ClusterId, taskType)
|
||||
@@ -86,6 +118,13 @@ func (this *HTTPDNSClusterService) FindHTTPDNSCluster(ctx context.Context, req *
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cluster != nil {
|
||||
_ = grpc.SetHeader(ctx, metadata.Pairs(
|
||||
"x-httpdns-auto-remote-start", fmt.Sprintf("%t", cluster.AutoRemoteStart),
|
||||
"x-httpdns-access-log-is-on", fmt.Sprintf("%t", cluster.AccessLogIsOn),
|
||||
"x-httpdns-time-zone", cluster.TimeZone,
|
||||
))
|
||||
}
|
||||
return &pb.FindHTTPDNSClusterResponse{Cluster: toPBCluster(cluster)}, nil
|
||||
}
|
||||
|
||||
@@ -107,10 +146,12 @@ func (this *HTTPDNSClusterService) ListHTTPDNSClusters(ctx context.Context, req
|
||||
|
||||
func (this *HTTPDNSClusterService) FindAllHTTPDNSClusters(ctx context.Context, req *pb.FindAllHTTPDNSClustersRequest) (*pb.FindAllHTTPDNSClustersResponse, error) {
|
||||
_, _, validateErr := this.ValidateAdminAndUser(ctx, true)
|
||||
isNode := false
|
||||
if validateErr != nil {
|
||||
if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
|
||||
return nil, validateErr
|
||||
}
|
||||
isNode = true
|
||||
}
|
||||
clusters, err := models.SharedHTTPDNSClusterDAO.FindAllEnabledClusters(this.NullTx())
|
||||
if err != nil {
|
||||
@@ -118,7 +159,12 @@ func (this *HTTPDNSClusterService) FindAllHTTPDNSClusters(ctx context.Context, r
|
||||
}
|
||||
var pbClusters []*pb.HTTPDNSCluster
|
||||
for _, cluster := range clusters {
|
||||
pbClusters = append(pbClusters, toPBCluster(cluster))
|
||||
if isNode {
|
||||
// 节点调用时解析证书引用,嵌入实际 PEM 数据
|
||||
pbClusters = append(pbClusters, toPBClusterWithResolvedCerts(this.NullTx(), cluster))
|
||||
} else {
|
||||
pbClusters = append(pbClusters, toPBCluster(cluster))
|
||||
}
|
||||
}
|
||||
return &pb.FindAllHTTPDNSClustersResponse{Clusters: pbClusters}, nil
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/goman"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/installers"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/setup"
|
||||
rpcutils "github.com/TeaOSLab/EdgeAPI/internal/rpc/utils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
@@ -247,6 +248,12 @@ func (this *HTTPDNSNodeService) DownloadHTTPDNSNodeInstallationFile(ctx context.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查自动升级开关
|
||||
upgradeConfig, _ := setup.LoadUpgradeConfig()
|
||||
if upgradeConfig != nil && !upgradeConfig.AutoUpgrade {
|
||||
return &pb.DownloadHTTPDNSNodeInstallationFileResponse{}, nil
|
||||
}
|
||||
|
||||
var file = installers.SharedDeployManager.FindHTTPDNSNodeFile(req.Os, req.Arch)
|
||||
if file == nil {
|
||||
return &pb.DownloadHTTPDNSNodeInstallationFileResponse{}, nil
|
||||
@@ -274,6 +281,120 @@ func (this *HTTPDNSNodeService) DownloadHTTPDNSNodeInstallationFile(ctx context.
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CountAllUpgradeHTTPDNSNodesWithClusterId 计算需要升级的HTTPDNS节点数量
|
||||
func (this *HTTPDNSNodeService) CountAllUpgradeHTTPDNSNodesWithClusterId(ctx context.Context, req *pb.CountAllUpgradeHTTPDNSNodesWithClusterIdRequest) (*pb.RPCCountResponse, error) {
|
||||
_, err := this.ValidateAdmin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tx = this.NullTx()
|
||||
deployFiles := installers.SharedDeployManager.LoadHTTPDNSNodeFiles()
|
||||
total := int64(0)
|
||||
for _, deployFile := range deployFiles {
|
||||
count, err := models.SharedHTTPDNSNodeDAO.CountAllLowerVersionNodesWithClusterId(tx, req.ClusterId, deployFile.OS, deployFile.Arch, deployFile.Version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
total += count
|
||||
}
|
||||
return this.SuccessCount(total)
|
||||
}
|
||||
|
||||
// FindAllUpgradeHTTPDNSNodesWithClusterId 列出所有需要升级的HTTPDNS节点
|
||||
func (this *HTTPDNSNodeService) FindAllUpgradeHTTPDNSNodesWithClusterId(ctx context.Context, req *pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest) (*pb.FindAllUpgradeHTTPDNSNodesWithClusterIdResponse, error) {
|
||||
_, err := this.ValidateAdmin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tx = this.NullTx()
|
||||
deployFiles := installers.SharedDeployManager.LoadHTTPDNSNodeFiles()
|
||||
var result []*pb.FindAllUpgradeHTTPDNSNodesWithClusterIdResponse_HTTPDNSNodeUpgrade
|
||||
for _, deployFile := range deployFiles {
|
||||
nodes, err := models.SharedHTTPDNSNodeDAO.FindAllLowerVersionNodesWithClusterId(tx, req.ClusterId, deployFile.OS, deployFile.Arch, deployFile.Version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
// 解析状态获取当前版本
|
||||
var oldVersion string
|
||||
if len(node.Status) > 0 {
|
||||
var statusMap map[string]interface{}
|
||||
if json.Unmarshal(node.Status, &statusMap) == nil {
|
||||
if v, ok := statusMap["buildVersion"]; ok {
|
||||
oldVersion, _ = v.(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pbNode := toPBNode(node)
|
||||
|
||||
// 认证信息
|
||||
login, loginErr := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(tx, nodeconfigs.NodeRoleHTTPDNS, int64(node.Id))
|
||||
if loginErr != nil {
|
||||
return nil, loginErr
|
||||
}
|
||||
if login != nil && pbNode != nil {
|
||||
pbNode.NodeLogin = &pb.NodeLogin{
|
||||
Id: int64(login.Id),
|
||||
Name: login.Name,
|
||||
Type: login.Type,
|
||||
Params: login.Params,
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdResponse_HTTPDNSNodeUpgrade{
|
||||
Node: pbNode,
|
||||
Os: deployFile.OS,
|
||||
Arch: deployFile.Arch,
|
||||
OldVersion: oldVersion,
|
||||
NewVersion: deployFile.Version,
|
||||
})
|
||||
}
|
||||
}
|
||||
return &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdResponse{Nodes: result}, nil
|
||||
}
|
||||
|
||||
// UpgradeHTTPDNSNode 升级单个HTTPDNS节点
|
||||
func (this *HTTPDNSNodeService) UpgradeHTTPDNSNode(ctx context.Context, req *pb.UpgradeHTTPDNSNodeRequest) (*pb.RPCSuccess, error) {
|
||||
_, err := this.ValidateAdmin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tx = this.NullTx()
|
||||
|
||||
err = models.SharedHTTPDNSNodeDAO.UpdateNodeIsInstalled(tx, req.NodeId, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 重置安装状态
|
||||
installStatus, err := models.SharedHTTPDNSNodeDAO.FindNodeInstallStatus(tx, req.NodeId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if installStatus == nil {
|
||||
installStatus = &models.NodeInstallStatus{}
|
||||
}
|
||||
installStatus.IsOk = false
|
||||
installStatus.IsFinished = false
|
||||
err = models.SharedHTTPDNSNodeDAO.UpdateNodeInstallStatus(tx, req.NodeId, installStatus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
goman.New(func() {
|
||||
installErr := installers.SharedHTTPDNSNodeQueue().InstallNodeProcess(req.NodeId, true)
|
||||
if installErr != nil {
|
||||
logs.Println("[RPC][HTTPDNS]upgrade node failed:", installErr.Error())
|
||||
}
|
||||
})
|
||||
|
||||
return this.Success()
|
||||
}
|
||||
|
||||
func shouldTriggerHTTPDNSInstall(installStatusJSON []byte) bool {
|
||||
if len(installStatusJSON) == 0 {
|
||||
return false
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/goman"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/installers"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/setup"
|
||||
rpcutils "github.com/TeaOSLab/EdgeAPI/internal/rpc/utils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
@@ -484,6 +485,12 @@ func (this *NSNodeService) DownloadNSNodeInstallationFile(ctx context.Context, r
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查自动升级开关
|
||||
upgradeConfig, _ := setup.LoadUpgradeConfig()
|
||||
if upgradeConfig != nil && !upgradeConfig.AutoUpgrade {
|
||||
return &pb.DownloadNSNodeInstallationFileResponse{}, nil
|
||||
}
|
||||
|
||||
var file = installers.SharedDeployManager.FindNSNodeFile(req.Os, req.Arch)
|
||||
if file == nil {
|
||||
return &pb.DownloadNSNodeInstallationFileResponse{}, nil
|
||||
@@ -738,3 +745,109 @@ func (this *NSNodeService) UpdateNSNodeAPIConfig(ctx context.Context, req *pb.Up
|
||||
|
||||
return this.Success()
|
||||
}
|
||||
|
||||
// FindAllUpgradeNSNodesWithNSClusterId 列出所有需要升级的NS节点
|
||||
func (this *NSNodeService) FindAllUpgradeNSNodesWithNSClusterId(ctx context.Context, req *pb.FindAllUpgradeNSNodesWithNSClusterIdRequest) (*pb.FindAllUpgradeNSNodesWithNSClusterIdResponse, error) {
|
||||
_, err := this.ValidateAdmin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tx = this.NullTx()
|
||||
deployFiles := installers.SharedDeployManager.LoadNSNodeFiles()
|
||||
var result []*pb.FindAllUpgradeNSNodesWithNSClusterIdResponse_NSNodeUpgrade
|
||||
for _, deployFile := range deployFiles {
|
||||
nodes, err := models.SharedNSNodeDAO.FindAllLowerVersionNodesWithClusterId(tx, req.NsClusterId, deployFile.OS, deployFile.Arch, deployFile.Version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
// 解析状态获取当前版本
|
||||
var oldVersion string
|
||||
if len(node.Status) > 0 {
|
||||
var statusMap map[string]interface{}
|
||||
if json.Unmarshal(node.Status, &statusMap) == nil {
|
||||
if v, ok := statusMap["buildVersion"]; ok {
|
||||
oldVersion, _ = v.(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 安装信息
|
||||
installStatus, installErr := node.DecodeInstallStatus()
|
||||
if installErr != nil {
|
||||
return nil, installErr
|
||||
}
|
||||
pbInstallStatus := &pb.NodeInstallStatus{}
|
||||
if installStatus != nil {
|
||||
pbInstallStatus = &pb.NodeInstallStatus{
|
||||
IsRunning: installStatus.IsRunning,
|
||||
IsFinished: installStatus.IsFinished,
|
||||
IsOk: installStatus.IsOk,
|
||||
Error: installStatus.Error,
|
||||
ErrorCode: installStatus.ErrorCode,
|
||||
UpdatedAt: installStatus.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// 认证信息
|
||||
login, loginErr := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(tx, nodeconfigs.NodeRoleDNS, int64(node.Id))
|
||||
if loginErr != nil {
|
||||
return nil, loginErr
|
||||
}
|
||||
var pbLogin *pb.NodeLogin
|
||||
if login != nil {
|
||||
pbLogin = &pb.NodeLogin{
|
||||
Id: int64(login.Id),
|
||||
Name: login.Name,
|
||||
Type: login.Type,
|
||||
Params: login.Params,
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, &pb.FindAllUpgradeNSNodesWithNSClusterIdResponse_NSNodeUpgrade{
|
||||
NsNode: &pb.NSNode{
|
||||
Id: int64(node.Id),
|
||||
Name: node.Name,
|
||||
IsOn: node.IsOn,
|
||||
UniqueId: node.UniqueId,
|
||||
IsInstalled: node.IsInstalled,
|
||||
IsUp: node.IsUp,
|
||||
IsActive: node.IsActive,
|
||||
StatusJSON: node.Status,
|
||||
InstallStatus: pbInstallStatus,
|
||||
NodeLogin: pbLogin,
|
||||
},
|
||||
Os: deployFile.OS,
|
||||
Arch: deployFile.Arch,
|
||||
OldVersion: oldVersion,
|
||||
NewVersion: deployFile.Version,
|
||||
})
|
||||
}
|
||||
}
|
||||
return &pb.FindAllUpgradeNSNodesWithNSClusterIdResponse{Nodes: result}, nil
|
||||
}
|
||||
|
||||
// UpgradeNSNode 升级单个NS节点
|
||||
func (this *NSNodeService) UpgradeNSNode(ctx context.Context, req *pb.UpgradeNSNodeRequest) (*pb.RPCSuccess, error) {
|
||||
_, err := this.ValidateAdmin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tx = this.NullTx()
|
||||
|
||||
err = models.SharedNSNodeDAO.UpdateNodeIsInstalled(tx, req.NsNodeId, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
goman.New(func() {
|
||||
installErr := installers.SharedNSNodeQueue().InstallNodeProcess(req.NsNodeId, true)
|
||||
if installErr != nil {
|
||||
logs.Println("[RPC]upgrade dns node:" + installErr.Error())
|
||||
}
|
||||
})
|
||||
|
||||
return this.Success()
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/installers"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/remotelogs"
|
||||
rpcutils "github.com/TeaOSLab/EdgeAPI/internal/rpc/utils"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/setup"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils/numberutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
|
||||
@@ -1716,6 +1717,12 @@ func (this *NodeService) DownloadNodeInstallationFile(ctx context.Context, req *
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查自动升级开关
|
||||
upgradeConfig, _ := setup.LoadUpgradeConfig()
|
||||
if upgradeConfig != nil && !upgradeConfig.AutoUpgrade {
|
||||
return &pb.DownloadNodeInstallationFileResponse{}, nil
|
||||
}
|
||||
|
||||
var file = installers.SharedDeployManager.FindNodeFile(req.Os, req.Arch)
|
||||
if file == nil {
|
||||
return &pb.DownloadNodeInstallationFileResponse{}, nil
|
||||
|
||||
@@ -113,6 +113,9 @@ var upgradeFuncs = []*upgradeVersion{
|
||||
{
|
||||
"1.4.8", upgradeV1_4_8,
|
||||
},
|
||||
{
|
||||
"1.4.9", upgradeV1_4_9,
|
||||
},
|
||||
}
|
||||
|
||||
// UpgradeSQLData 升级SQL数据
|
||||
@@ -1274,10 +1277,25 @@ func upgradeV1_4_8(db *dbs.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 1.4.9
|
||||
func upgradeV1_4_9(db *dbs.DB) error {
|
||||
_, err := db.Exec("ALTER TABLE `edgeHTTPDNSClusters` ALTER COLUMN `installDir` SET DEFAULT '/root/edge-httpdns'")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.Exec("ALTER TABLE `edgeHTTPDNSNodes` ALTER COLUMN `installDir` SET DEFAULT '/root/edge-httpdns'")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createHTTPDNSTables(db *dbs.DB) error {
|
||||
sqls := []string{
|
||||
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSClusters` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`isDefault` tinyint unsigned DEFAULT '0',`serviceDomain` varchar(255) DEFAULT NULL,`defaultTTL` int unsigned DEFAULT '30',`fallbackTimeoutMs` int unsigned DEFAULT '300',`installDir` varchar(255) DEFAULT '/opt/edge-httpdns',`tlsPolicy` json DEFAULT NULL,`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),KEY `name` (`name`),KEY `isDefault` (`isDefault`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS集群配置表(默认TTL、回退超时、服务域名等)'",
|
||||
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSNodes` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`clusterId` bigint unsigned DEFAULT '0',`name` varchar(255) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`isUp` tinyint unsigned DEFAULT '0',`isInstalled` tinyint unsigned DEFAULT '0',`isActive` tinyint unsigned DEFAULT '0',`uniqueId` varchar(64) DEFAULT NULL,`secret` varchar(64) DEFAULT NULL,`installDir` varchar(255) DEFAULT '/opt/edge-httpdns',`status` json DEFAULT NULL,`installStatus` json DEFAULT NULL,`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),UNIQUE KEY `uniqueId` (`uniqueId`),KEY `clusterId` (`clusterId`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS节点表(节点基础信息与运行状态)'",
|
||||
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSClusters` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`isDefault` tinyint unsigned DEFAULT '0',`serviceDomain` varchar(255) DEFAULT NULL,`defaultTTL` int unsigned DEFAULT '30',`fallbackTimeoutMs` int unsigned DEFAULT '300',`installDir` varchar(255) DEFAULT '/root/edge-httpdns',`tlsPolicy` json DEFAULT NULL,`autoRemoteStart` tinyint unsigned DEFAULT '0',`accessLogIsOn` tinyint unsigned DEFAULT '0',`timeZone` varchar(128) NOT NULL DEFAULT '',`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),KEY `name` (`name`),KEY `isDefault` (`isDefault`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS集群配置表(默认TTL、回退超时、服务域名等)'",
|
||||
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSNodes` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`clusterId` bigint unsigned DEFAULT '0',`name` varchar(255) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`isUp` tinyint unsigned DEFAULT '0',`isInstalled` tinyint unsigned DEFAULT '0',`isActive` tinyint unsigned DEFAULT '0',`uniqueId` varchar(64) DEFAULT NULL,`secret` varchar(64) DEFAULT NULL,`installDir` varchar(255) DEFAULT '/root/edge-httpdns',`status` json DEFAULT NULL,`installStatus` json DEFAULT NULL,`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),UNIQUE KEY `uniqueId` (`uniqueId`),KEY `clusterId` (`clusterId`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS节点表(节点基础信息与运行状态)'",
|
||||
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSApps` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`appId` varchar(64) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`clusterIdsJSON` text DEFAULT NULL,`sniMode` varchar(64) DEFAULT 'fixed_hide',`userId` bigint unsigned DEFAULT '0',`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),UNIQUE KEY `appId` (`appId`),KEY `name` (`name`),KEY `userId` (`userId`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS应用表(应用与集群绑定关系)'",
|
||||
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSAppSecrets` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`appId` bigint unsigned DEFAULT '0',`signEnabled` tinyint unsigned DEFAULT '0',`signSecret` varchar(255) DEFAULT NULL,`signUpdatedAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),UNIQUE KEY `appId` (`appId`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS应用密钥表(请求验签开关与加签Secret)'",
|
||||
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSDomains` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`appId` bigint unsigned DEFAULT '0',`domain` varchar(255) DEFAULT NULL,`isOn` tinyint unsigned DEFAULT '1',`createdAt` bigint unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),UNIQUE KEY `appId_domain` (`appId`,`domain`),KEY `domain` (`domain`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS应用域名表(应用绑定的业务域名)'",
|
||||
|
||||
45
EdgeAPI/internal/setup/upgrade_config.go
Normal file
45
EdgeAPI/internal/setup/upgrade_config.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
|
||||
)
|
||||
|
||||
var (
|
||||
sharedUpgradeConfig *systemconfigs.UpgradeConfig
|
||||
sharedUpgradeConfigTime time.Time
|
||||
sharedUpgradeConfigMu sync.Mutex
|
||||
)
|
||||
|
||||
const upgradeConfigTTL = 5 * time.Minute
|
||||
|
||||
// LoadUpgradeConfig 读取升级配置(带5分钟内存缓存)
|
||||
func LoadUpgradeConfig() (*systemconfigs.UpgradeConfig, error) {
|
||||
sharedUpgradeConfigMu.Lock()
|
||||
defer sharedUpgradeConfigMu.Unlock()
|
||||
|
||||
if sharedUpgradeConfig != nil && time.Since(sharedUpgradeConfigTime) < upgradeConfigTTL {
|
||||
return sharedUpgradeConfig, nil
|
||||
}
|
||||
|
||||
valueJSON, err := models.SharedSysSettingDAO.ReadSetting(nil, systemconfigs.SettingCodeUpgradeConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := systemconfigs.NewUpgradeConfig()
|
||||
if len(valueJSON) > 0 {
|
||||
err = json.Unmarshal(valueJSON, config)
|
||||
if err != nil {
|
||||
return config, nil
|
||||
}
|
||||
}
|
||||
|
||||
sharedUpgradeConfig = config
|
||||
sharedUpgradeConfigTime = time.Now()
|
||||
return config, nil
|
||||
}
|
||||
107
EdgeAPI/internal/tasks/httpdns_node_monitor_task.go
Normal file
107
EdgeAPI/internal/tasks/httpdns_node_monitor_task.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/goman"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/installers"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
)
|
||||
|
||||
func init() {
|
||||
dbs.OnReadyDone(func() {
|
||||
goman.New(func() {
|
||||
NewHTTPDNSNodeMonitorTask(1 * time.Minute).Start()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type httpdnsNodeStartingTry struct {
|
||||
count int
|
||||
timestamp int64
|
||||
}
|
||||
|
||||
// HTTPDNSNodeMonitorTask monitors HTTPDNS node activity and optionally tries to start offline nodes.
|
||||
type HTTPDNSNodeMonitorTask struct {
|
||||
BaseTask
|
||||
|
||||
ticker *time.Ticker
|
||||
|
||||
recoverMap map[int64]*httpdnsNodeStartingTry // nodeId => retry info
|
||||
}
|
||||
|
||||
func NewHTTPDNSNodeMonitorTask(duration time.Duration) *HTTPDNSNodeMonitorTask {
|
||||
return &HTTPDNSNodeMonitorTask{
|
||||
ticker: time.NewTicker(duration),
|
||||
recoverMap: map[int64]*httpdnsNodeStartingTry{},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *HTTPDNSNodeMonitorTask) Start() {
|
||||
for range t.ticker.C {
|
||||
if err := t.Loop(); err != nil {
|
||||
t.logErr("HTTPDNS_NODE_MONITOR", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *HTTPDNSNodeMonitorTask) Loop() error {
|
||||
// only run on primary api node
|
||||
if !t.IsPrimaryNode() {
|
||||
return nil
|
||||
}
|
||||
|
||||
clusters, err := models.SharedHTTPDNSClusterDAO.FindAllEnabledClusters(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cluster := range clusters {
|
||||
if cluster == nil || !cluster.IsOn || !cluster.AutoRemoteStart {
|
||||
continue
|
||||
}
|
||||
clusterID := int64(cluster.Id)
|
||||
inactiveNodes, err := models.SharedHTTPDNSNodeDAO.FindAllInactiveNodesWithClusterId(nil, clusterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(inactiveNodes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
nodeQueue := installers.NewHTTPDNSNodeQueue()
|
||||
for _, node := range inactiveNodes {
|
||||
nodeID := int64(node.Id)
|
||||
tryInfo, ok := t.recoverMap[nodeID]
|
||||
if !ok {
|
||||
tryInfo = &httpdnsNodeStartingTry{
|
||||
count: 1,
|
||||
timestamp: time.Now().Unix(),
|
||||
}
|
||||
t.recoverMap[nodeID] = tryInfo
|
||||
} else {
|
||||
if tryInfo.count >= 3 {
|
||||
if tryInfo.timestamp+10*60 > time.Now().Unix() {
|
||||
continue
|
||||
}
|
||||
tryInfo.timestamp = time.Now().Unix()
|
||||
tryInfo.count = 0
|
||||
}
|
||||
tryInfo.count++
|
||||
}
|
||||
|
||||
err = nodeQueue.StartNode(nodeID)
|
||||
if err != nil {
|
||||
if !installers.IsGrantError(err) {
|
||||
_ = models.SharedNodeLogDAO.CreateLog(nil, nodeconfigs.NodeRoleHTTPDNS, nodeID, 0, 0, models.LevelError, "NODE", "start node from remote API failed: "+err.Error(), time.Now().Unix(), "", nil)
|
||||
}
|
||||
continue
|
||||
}
|
||||
_ = models.SharedNodeLogDAO.CreateLog(nil, nodeconfigs.NodeRoleHTTPDNS, nodeID, 0, 0, models.LevelSuccess, "NODE", "start node from remote API successfully", time.Now().Unix(), "", nil)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
function verify_components_bundle() {
|
||||
local file_path="$1"
|
||||
if [ ! -f "$file_path" ]; then
|
||||
echo "[error] components.js not found: $file_path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local file_size
|
||||
file_size=$(wc -c < "$file_path")
|
||||
if [ "$file_size" -lt 100000 ]; then
|
||||
echo "[error] components.js looks too small ($file_size bytes), generate likely failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! grep -q 'Vue.component("csrf-token"' "$file_path"; then
|
||||
echo "[error] components.js missing csrf-token component, generate likely failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "verify components.js: ok ($file_size bytes)"
|
||||
return 0
|
||||
}
|
||||
|
||||
function build() {
|
||||
ROOT=$(dirname "$0")
|
||||
@@ -58,7 +82,7 @@ function build() {
|
||||
|
||||
# generate files
|
||||
echo "generating files ..."
|
||||
env CGO_ENABLED=0 go run -tags $TAG "$ROOT"/../cmd/edge-admin/main.go generate
|
||||
env TEAROOT="$ROOT" CGO_ENABLED=0 go run -tags "$TAG" "$ROOT"/../cmd/edge-admin/main.go generate
|
||||
if [ "$(which uglifyjs)" ]; then
|
||||
echo "compress to component.js ..."
|
||||
uglifyjs --compress --mangle -- "${JS_ROOT}"/components.src.js > "${JS_ROOT}"/components.js
|
||||
@@ -69,6 +93,8 @@ function build() {
|
||||
cp "${JS_ROOT}"/utils.js "${JS_ROOT}"/utils.min.js
|
||||
fi
|
||||
|
||||
verify_components_bundle "${JS_ROOT}/components.js"
|
||||
|
||||
# create dir & copy files
|
||||
echo "copying ..."
|
||||
if [ ! -d "$DIST" ]; then
|
||||
|
||||
@@ -1,22 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
JS_ROOT=../web/public/js
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
JS_ROOT="$ROOT"/../web/public/js
|
||||
|
||||
function verify_components_bundle() {
|
||||
local file_path="$1"
|
||||
if [ ! -f "$file_path" ]; then
|
||||
echo "[error] components.js not found: $file_path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local file_size
|
||||
file_size=$(wc -c < "$file_path")
|
||||
if [ "$file_size" -lt 100000 ]; then
|
||||
echo "[error] components.js looks too small ($file_size bytes), generate likely failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! grep -q 'Vue.component("csrf-token"' "$file_path"; then
|
||||
echo "[error] components.js missing csrf-token component, generate likely failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "verify components.js: ok ($file_size bytes)"
|
||||
return 0
|
||||
}
|
||||
|
||||
echo "generating component.src.js ..."
|
||||
env CGO_ENABLED=0 go run -tags=community ../cmd/edge-admin/main.go generate
|
||||
env TEAROOT="$ROOT" CGO_ENABLED=0 go run -tags=community "$ROOT"/../cmd/edge-admin/main.go generate
|
||||
|
||||
if [ "$(which uglifyjs)" ]; then
|
||||
echo "compress to component.js ..."
|
||||
uglifyjs --compress --mangle -- ${JS_ROOT}/components.src.js > ${JS_ROOT}/components.js
|
||||
uglifyjs --compress --mangle -- "${JS_ROOT}"/components.src.js > "${JS_ROOT}"/components.js
|
||||
|
||||
echo "compress to utils.min.js ..."
|
||||
uglifyjs --compress --mangle -- ${JS_ROOT}/utils.js > ${JS_ROOT}/utils.min.js
|
||||
uglifyjs --compress --mangle -- "${JS_ROOT}"/utils.js > "${JS_ROOT}"/utils.min.js
|
||||
else
|
||||
echo "copy to component.js ..."
|
||||
cp ${JS_ROOT}/components.src.js ${JS_ROOT}/components.js
|
||||
cp "${JS_ROOT}"/components.src.js "${JS_ROOT}"/components.js
|
||||
|
||||
echo "copy to utils.min.js ..."
|
||||
cp ${JS_ROOT}/utils.js ${JS_ROOT}/utils.min.js
|
||||
cp "${JS_ROOT}"/utils.js "${JS_ROOT}"/utils.min.js
|
||||
fi
|
||||
|
||||
echo "ok"
|
||||
verify_components_bundle "${JS_ROOT}/components.js"
|
||||
|
||||
echo "ok"
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -112,10 +113,12 @@ func main() {
|
||||
}
|
||||
})
|
||||
app.On("generate", func() {
|
||||
prepareGenerateRoot()
|
||||
|
||||
err := gen.Generate()
|
||||
if err != nil {
|
||||
fmt.Println("generate failed: " + err.Error())
|
||||
return
|
||||
os.Exit(1)
|
||||
}
|
||||
})
|
||||
app.On("dev", func() {
|
||||
@@ -214,3 +217,32 @@ func main() {
|
||||
adminNode.Run()
|
||||
})
|
||||
}
|
||||
|
||||
func prepareGenerateRoot() {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
candidates := []string{
|
||||
wd,
|
||||
filepath.Clean(filepath.Join(wd, "..")),
|
||||
}
|
||||
|
||||
for _, root := range candidates {
|
||||
componentsDir := filepath.Join(root, "web", "public", "js", "components")
|
||||
stat, statErr := os.Stat(componentsDir)
|
||||
if statErr != nil || !stat.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// In testing mode, generator reads from Tea.Root + "/../web/...",
|
||||
// so keep Root under build dir to make relative path stable.
|
||||
buildRoot := filepath.Join(root, "build")
|
||||
Tea.UpdateRoot(buildRoot)
|
||||
Tea.SetPublicDir(filepath.Join(root, "web", "public"))
|
||||
Tea.SetViewsDir(filepath.Join(root, "web", "views"))
|
||||
Tea.SetTmpDir(filepath.Join(root, "web", "tmp"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
69
EdgeAdmin/internal/configloaders/upgrade_config.go
Normal file
69
EdgeAdmin/internal/configloaders/upgrade_config.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package configloaders
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
|
||||
)
|
||||
|
||||
const UpgradeSettingName = "upgradeConfig"
|
||||
|
||||
var sharedUpgradeConfig *systemconfigs.UpgradeConfig
|
||||
|
||||
func LoadUpgradeConfig() (*systemconfigs.UpgradeConfig, error) {
|
||||
locker.Lock()
|
||||
defer locker.Unlock()
|
||||
|
||||
if sharedUpgradeConfig != nil {
|
||||
return sharedUpgradeConfig, nil
|
||||
}
|
||||
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := rpcClient.SysSettingRPC().ReadSysSetting(rpcClient.Context(0), &pb.ReadSysSettingRequest{
|
||||
Code: UpgradeSettingName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resp.ValueJSON) == 0 {
|
||||
sharedUpgradeConfig = systemconfigs.NewUpgradeConfig()
|
||||
return sharedUpgradeConfig, nil
|
||||
}
|
||||
|
||||
config := systemconfigs.NewUpgradeConfig()
|
||||
err = json.Unmarshal(resp.ValueJSON, config)
|
||||
if err != nil {
|
||||
sharedUpgradeConfig = systemconfigs.NewUpgradeConfig()
|
||||
return sharedUpgradeConfig, nil
|
||||
}
|
||||
sharedUpgradeConfig = config
|
||||
return sharedUpgradeConfig, nil
|
||||
}
|
||||
|
||||
func UpdateUpgradeConfig(config *systemconfigs.UpgradeConfig) error {
|
||||
locker.Lock()
|
||||
defer locker.Unlock()
|
||||
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
valueJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = rpcClient.SysSettingRPC().UpdateSysSetting(rpcClient.Context(0), &pb.UpdateSysSettingRequest{
|
||||
Code: UpgradeSettingName,
|
||||
ValueJSON: valueJSON,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sharedUpgradeConfig = config
|
||||
return nil
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package teaconst
|
||||
|
||||
const (
|
||||
Version = "1.4.8" //1.3.9
|
||||
Version = "1.4.9" //1.3.9
|
||||
|
||||
APINodeVersion = "1.4.8" //1.3.9
|
||||
APINodeVersion = "1.4.9" //1.3.9
|
||||
|
||||
ProductName = "Edge Admin"
|
||||
ProcessName = "edge-admin"
|
||||
|
||||
@@ -28,19 +28,20 @@ func (this *SdkCheckAction) RunGet(params struct {
|
||||
return
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(params.Version)
|
||||
t := strings.ToLower(strings.TrimSpace(params.Type))
|
||||
if t == "doc" {
|
||||
docPath := findUploadedSDKDocPath(platform, params.Version)
|
||||
docPath := findUploadedSDKDocPath(platform, version)
|
||||
if len(docPath) == 0 {
|
||||
this.Data["exists"] = false
|
||||
this.Data["message"] = "Documentation is unavailable, please upload first"
|
||||
this.Data["message"] = "当前平台/版本尚未上传集成文档"
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
downloadURL := "/httpdns/apps/sdk/doc?platform=" + url.QueryEscape(platform)
|
||||
if len(strings.TrimSpace(params.Version)) > 0 {
|
||||
downloadURL += "&version=" + url.QueryEscape(strings.TrimSpace(params.Version))
|
||||
if len(version) > 0 {
|
||||
downloadURL += "&version=" + url.QueryEscape(version)
|
||||
}
|
||||
this.Data["exists"] = true
|
||||
this.Data["url"] = downloadURL
|
||||
@@ -48,17 +49,17 @@ func (this *SdkCheckAction) RunGet(params struct {
|
||||
return
|
||||
}
|
||||
|
||||
archivePath := findSDKArchivePath(filename, params.Version)
|
||||
archivePath := findSDKArchivePath(filename, version)
|
||||
if len(archivePath) == 0 {
|
||||
this.Data["exists"] = false
|
||||
this.Data["message"] = "SDK package is unavailable, please upload first"
|
||||
this.Data["message"] = "当前平台/版本尚未上传 SDK 安装包"
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
downloadURL := "/httpdns/apps/sdk/download?platform=" + url.QueryEscape(platform)
|
||||
if len(strings.TrimSpace(params.Version)) > 0 {
|
||||
downloadURL += "&version=" + url.QueryEscape(strings.TrimSpace(params.Version))
|
||||
if len(version) > 0 {
|
||||
downloadURL += "&version=" + url.QueryEscape(version)
|
||||
}
|
||||
downloadURL += "&raw=1"
|
||||
this.Data["exists"] = true
|
||||
|
||||
@@ -3,7 +3,6 @@ package apps
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
)
|
||||
@@ -20,7 +19,7 @@ func (this *SdkDocAction) RunGet(params struct {
|
||||
Platform string
|
||||
Version string
|
||||
}) {
|
||||
platform, _, readmeRelativePath, _, err := resolveSDKPlatform(params.Platform)
|
||||
platform, _, _, _, err := resolveSDKPlatform(params.Platform)
|
||||
if err != nil {
|
||||
this.Data["exists"] = false
|
||||
this.Data["message"] = err.Error()
|
||||
@@ -28,35 +27,25 @@ func (this *SdkDocAction) RunGet(params struct {
|
||||
return
|
||||
}
|
||||
|
||||
var data []byte
|
||||
uploadedDocPath := findUploadedSDKDocPath(platform, params.Version)
|
||||
if len(uploadedDocPath) > 0 {
|
||||
data, err = os.ReadFile(uploadedDocPath)
|
||||
}
|
||||
|
||||
sdkRoot, sdkRootErr := findSDKRoot()
|
||||
if len(data) == 0 && sdkRootErr == nil {
|
||||
readmePath := filepath.Join(sdkRoot, readmeRelativePath)
|
||||
data, err = os.ReadFile(readmePath)
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
localDocPath := findLocalSDKDocPath(platform)
|
||||
if len(localDocPath) > 0 {
|
||||
data, err = os.ReadFile(localDocPath)
|
||||
}
|
||||
}
|
||||
|
||||
if len(data) == 0 || err != nil {
|
||||
docPath := findUploadedSDKDocPath(platform, params.Version)
|
||||
if len(docPath) == 0 {
|
||||
this.Data["exists"] = false
|
||||
this.Data["message"] = "SDK documentation is not found on server, please upload first"
|
||||
this.Data["message"] = "当前平台/版本尚未上传集成文档"
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
downloadName := filepath.Base(uploadedDocPath)
|
||||
data, err := os.ReadFile(docPath)
|
||||
if err != nil || len(data) == 0 {
|
||||
this.Data["exists"] = false
|
||||
this.Data["message"] = "读取集成文档失败"
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
downloadName := filepath.Base(docPath)
|
||||
if len(downloadName) == 0 || downloadName == "." || downloadName == string(filepath.Separator) {
|
||||
downloadName = "httpdns-sdk-" + strings.ToLower(platform) + ".md"
|
||||
downloadName = "sdk-doc.md"
|
||||
}
|
||||
|
||||
this.AddHeader("Content-Type", "text/markdown; charset=utf-8")
|
||||
|
||||
@@ -32,7 +32,7 @@ func (this *SdkDownloadAction) RunGet(params struct {
|
||||
archivePath := findSDKArchivePath(filename, params.Version)
|
||||
if len(archivePath) == 0 {
|
||||
this.Data["exists"] = false
|
||||
this.Data["message"] = "SDK archive not found on server, please upload first: " + filename
|
||||
this.Data["message"] = "当前平台/版本尚未上传 SDK 安装包"
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
@@ -40,7 +40,7 @@ func (this *SdkDownloadAction) RunGet(params struct {
|
||||
fp, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
this.Data["exists"] = false
|
||||
this.Data["message"] = "failed to open SDK archive: " + err.Error()
|
||||
this.Data["message"] = "打开 SDK 安装包失败: " + err.Error()
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,16 +1,183 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
)
|
||||
|
||||
type sdkUploadMeta struct {
|
||||
Platform string `json:"platform"`
|
||||
Version string `json:"version"`
|
||||
FileType string `json:"fileType"` // sdk | doc
|
||||
Filename string `json:"filename"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type sdkUploadMetaRecord struct {
|
||||
Meta sdkUploadMeta
|
||||
Dir string
|
||||
FilePath string
|
||||
}
|
||||
|
||||
func sdkUploadMetaFilename(platform string, version string, fileType string) string {
|
||||
platform = strings.ToLower(strings.TrimSpace(platform))
|
||||
version = strings.TrimSpace(version)
|
||||
fileType = strings.ToLower(strings.TrimSpace(fileType))
|
||||
return ".httpdns-sdk-meta-" + platform + "-v" + version + "-" + fileType + ".json"
|
||||
}
|
||||
|
||||
func isSDKUploadMetaFile(name string) bool {
|
||||
name = strings.ToLower(strings.TrimSpace(name))
|
||||
return strings.HasPrefix(name, ".httpdns-sdk-meta-") && strings.HasSuffix(name, ".json")
|
||||
}
|
||||
|
||||
func parseSDKPlatformFromDownloadFilename(downloadFilename string) string {
|
||||
name := strings.ToLower(strings.TrimSpace(downloadFilename))
|
||||
if !strings.HasPrefix(name, "httpdns-sdk-") || !strings.HasSuffix(name, ".zip") {
|
||||
return ""
|
||||
}
|
||||
|
||||
platform := strings.TrimSuffix(strings.TrimPrefix(name, "httpdns-sdk-"), ".zip")
|
||||
switch platform {
|
||||
case "android", "ios", "flutter":
|
||||
return platform
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func listSDKUploadMetaRecords() []sdkUploadMetaRecord {
|
||||
type wrapped struct {
|
||||
record sdkUploadMetaRecord
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
byKey := map[string]wrapped{}
|
||||
for _, dir := range sdkUploadSearchDirs() {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := entry.Name()
|
||||
if !isSDKUploadMetaFile(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
metaPath := filepath.Join(dir, name)
|
||||
data, err := os.ReadFile(metaPath)
|
||||
if err != nil || len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var meta sdkUploadMeta
|
||||
if err = json.Unmarshal(data, &meta); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
meta.Platform = strings.ToLower(strings.TrimSpace(meta.Platform))
|
||||
meta.Version = strings.TrimSpace(meta.Version)
|
||||
meta.FileType = strings.ToLower(strings.TrimSpace(meta.FileType))
|
||||
meta.Filename = filepath.Base(strings.TrimSpace(meta.Filename))
|
||||
if len(meta.Platform) == 0 || len(meta.Version) == 0 || len(meta.Filename) == 0 {
|
||||
continue
|
||||
}
|
||||
if meta.FileType != "sdk" && meta.FileType != "doc" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(meta.Filename, "..") || strings.Contains(meta.Filename, "/") || strings.Contains(meta.Filename, "\\") {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(dir, meta.Filename)
|
||||
fileStat, err := os.Stat(filePath)
|
||||
if err != nil || fileStat.IsDir() || fileStat.Size() <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
metaStat, err := os.Stat(metaPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if meta.UpdatedAt <= 0 {
|
||||
meta.UpdatedAt = metaStat.ModTime().Unix()
|
||||
}
|
||||
|
||||
key := meta.Platform + "|" + meta.Version + "|" + meta.FileType
|
||||
current := wrapped{
|
||||
record: sdkUploadMetaRecord{
|
||||
Meta: meta,
|
||||
Dir: dir,
|
||||
FilePath: filePath,
|
||||
},
|
||||
modTime: metaStat.ModTime(),
|
||||
}
|
||||
old, ok := byKey[key]
|
||||
if !ok ||
|
||||
current.record.Meta.UpdatedAt > old.record.Meta.UpdatedAt ||
|
||||
(current.record.Meta.UpdatedAt == old.record.Meta.UpdatedAt && current.modTime.After(old.modTime)) ||
|
||||
(current.record.Meta.UpdatedAt == old.record.Meta.UpdatedAt && current.modTime.Equal(old.modTime) && current.record.FilePath > old.record.FilePath) {
|
||||
byKey[key] = current
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]sdkUploadMetaRecord, 0, len(byKey))
|
||||
for _, item := range byKey {
|
||||
result = append(result, item.record)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func findSDKUploadFileByMeta(platform string, version string, fileType string) string {
|
||||
platform = strings.ToLower(strings.TrimSpace(platform))
|
||||
version = strings.TrimSpace(version)
|
||||
fileType = strings.ToLower(strings.TrimSpace(fileType))
|
||||
if len(platform) == 0 || len(version) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, record := range listSDKUploadMetaRecords() {
|
||||
if record.Meta.Platform == platform && record.Meta.Version == version && record.Meta.FileType == fileType {
|
||||
return record.FilePath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findNewestSDKUploadFileByMeta(platform string, fileType string) string {
|
||||
platform = strings.ToLower(strings.TrimSpace(platform))
|
||||
fileType = strings.ToLower(strings.TrimSpace(fileType))
|
||||
if len(platform) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var foundPath string
|
||||
var foundUpdatedAt int64
|
||||
for _, record := range listSDKUploadMetaRecords() {
|
||||
if record.Meta.Platform != platform || record.Meta.FileType != fileType {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(foundPath) == 0 || record.Meta.UpdatedAt > foundUpdatedAt || (record.Meta.UpdatedAt == foundUpdatedAt && record.FilePath > foundPath) {
|
||||
foundPath = record.FilePath
|
||||
foundUpdatedAt = record.Meta.UpdatedAt
|
||||
}
|
||||
}
|
||||
return foundPath
|
||||
}
|
||||
|
||||
func sdkUploadDir() string {
|
||||
dirs := sdkUploadDirs()
|
||||
if len(dirs) > 0 {
|
||||
@@ -27,6 +194,7 @@ func sdkUploadDirs() []string {
|
||||
filepath.Clean(Tea.Root + "/../edge-user/data/httpdns/sdk"),
|
||||
filepath.Clean(Tea.Root + "/../../data/httpdns/sdk"),
|
||||
}
|
||||
|
||||
results := make([]string, 0, len(candidates))
|
||||
seen := map[string]bool{}
|
||||
for _, dir := range candidates {
|
||||
@@ -54,67 +222,6 @@ func sdkUploadSearchDirs() []string {
|
||||
return results
|
||||
}
|
||||
|
||||
func findFirstExistingDir(paths []string) string {
|
||||
for _, path := range paths {
|
||||
stat, err := os.Stat(path)
|
||||
if err == nil && stat.IsDir() {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findFirstExistingFile(paths []string) string {
|
||||
for _, path := range paths {
|
||||
stat, err := os.Stat(path)
|
||||
if err == nil && !stat.IsDir() && stat.Size() > 0 {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findNewestExistingFile(paths []string) string {
|
||||
type fileInfo struct {
|
||||
path string
|
||||
modTime time.Time
|
||||
}
|
||||
result := fileInfo{}
|
||||
for _, path := range paths {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil || stat.IsDir() {
|
||||
continue
|
||||
}
|
||||
if stat.Size() <= 0 {
|
||||
continue
|
||||
}
|
||||
if len(result.path) == 0 || stat.ModTime().After(result.modTime) || (stat.ModTime().Equal(result.modTime) && path > result.path) {
|
||||
result.path = path
|
||||
result.modTime = stat.ModTime()
|
||||
}
|
||||
}
|
||||
return result.path
|
||||
}
|
||||
|
||||
func findSDKRoot() (string, error) {
|
||||
candidates := []string{
|
||||
filepath.Clean(Tea.Root + "/EdgeHttpDNS/sdk"),
|
||||
filepath.Clean(Tea.Root + "/edge-httpdns/sdk"),
|
||||
filepath.Clean(Tea.Root + "/edge-httpdns/edge-httpdns/sdk"),
|
||||
filepath.Clean(Tea.Root + "/../EdgeHttpDNS/sdk"),
|
||||
filepath.Clean(Tea.Root + "/../../EdgeHttpDNS/sdk"),
|
||||
filepath.Clean(Tea.Root + "/../edge-httpdns/sdk"),
|
||||
filepath.Clean(Tea.Root + "/../../edge-httpdns/sdk"),
|
||||
}
|
||||
|
||||
dir := findFirstExistingDir(candidates)
|
||||
if len(dir) > 0 {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
return "", errors.New("SDK files are not found on current server")
|
||||
}
|
||||
|
||||
func resolveSDKPlatform(platform string) (key string, relativeDir string, readmeRelativePath string, downloadFilename string, err error) {
|
||||
switch strings.ToLower(strings.TrimSpace(platform)) {
|
||||
case "android":
|
||||
@@ -122,52 +229,23 @@ func resolveSDKPlatform(platform string) (key string, relativeDir string, readme
|
||||
case "ios":
|
||||
return "ios", "ios", "ios/README.md", "httpdns-sdk-ios.zip", nil
|
||||
case "flutter":
|
||||
return "flutter", "flutter/aliyun_httpdns", "flutter/aliyun_httpdns/README.md", "httpdns-sdk-flutter.zip", nil
|
||||
return "flutter", "flutter/new_httpdns", "flutter/new_httpdns/README.md", "httpdns-sdk-flutter.zip", nil
|
||||
default:
|
||||
return "", "", "", "", errors.New("invalid platform, expected one of: android, ios, flutter")
|
||||
return "", "", "", "", errors.New("不支持的平台,可选值:android、ios、flutter")
|
||||
}
|
||||
}
|
||||
|
||||
func findSDKArchivePath(downloadFilename string, version string) string {
|
||||
searchDirs := sdkUploadSearchDirs()
|
||||
|
||||
normalizedVersion := strings.TrimSpace(version)
|
||||
base := strings.TrimSuffix(downloadFilename, ".zip")
|
||||
if len(normalizedVersion) > 0 {
|
||||
versionFiles := []string{}
|
||||
for _, dir := range searchDirs {
|
||||
versionFiles = append(versionFiles, filepath.Join(dir, base+"-v"+normalizedVersion+".zip"))
|
||||
}
|
||||
if path := findFirstExistingFile(versionFiles); len(path) > 0 {
|
||||
return path
|
||||
}
|
||||
platform := parseSDKPlatformFromDownloadFilename(downloadFilename)
|
||||
if len(platform) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
patternName := base + "-v*.zip"
|
||||
matches := []string{}
|
||||
for _, dir := range searchDirs {
|
||||
found, _ := filepath.Glob(filepath.Join(dir, patternName))
|
||||
for _, file := range found {
|
||||
stat, err := os.Stat(file)
|
||||
if err == nil && !stat.IsDir() {
|
||||
matches = append(matches, file)
|
||||
}
|
||||
}
|
||||
normalizedVersion := strings.TrimSpace(version)
|
||||
if len(normalizedVersion) > 0 {
|
||||
return findSDKUploadFileByMeta(platform, normalizedVersion, "sdk")
|
||||
}
|
||||
if len(matches) > 0 {
|
||||
return findNewestExistingFile(matches)
|
||||
}
|
||||
|
||||
exactFiles := []string{}
|
||||
for _, dir := range searchDirs {
|
||||
exactFiles = append(exactFiles, filepath.Join(dir, downloadFilename))
|
||||
}
|
||||
if path := findFirstExistingFile(exactFiles); len(path) > 0 {
|
||||
return path
|
||||
}
|
||||
|
||||
return ""
|
||||
return findNewestSDKUploadFileByMeta(platform, "sdk")
|
||||
}
|
||||
|
||||
func findUploadedSDKDocPath(platform string, version string) string {
|
||||
@@ -176,42 +254,9 @@ func findUploadedSDKDocPath(platform string, version string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
searchDirs := sdkUploadSearchDirs()
|
||||
normalizedVersion := strings.TrimSpace(version)
|
||||
if len(normalizedVersion) > 0 {
|
||||
exactVersion := []string{}
|
||||
for _, dir := range searchDirs {
|
||||
exactVersion = append(exactVersion, filepath.Join(dir, "httpdns-sdk-"+platform+"-v"+normalizedVersion+".md"))
|
||||
}
|
||||
if file := findFirstExistingFile(exactVersion); len(file) > 0 {
|
||||
return file
|
||||
}
|
||||
return ""
|
||||
return findSDKUploadFileByMeta(platform, normalizedVersion, "doc")
|
||||
}
|
||||
|
||||
matches := []string{}
|
||||
for _, dir := range searchDirs {
|
||||
pattern := filepath.Join(dir, "httpdns-sdk-"+platform+"-v*.md")
|
||||
found, _ := filepath.Glob(pattern)
|
||||
matches = append(matches, found...)
|
||||
}
|
||||
if len(matches) > 0 {
|
||||
sort.Strings(matches)
|
||||
return findNewestExistingFile(matches)
|
||||
}
|
||||
|
||||
exact := []string{}
|
||||
for _, dir := range searchDirs {
|
||||
exact = append(exact, filepath.Join(dir, "httpdns-sdk-"+platform+".md"))
|
||||
}
|
||||
return findFirstExistingFile(exact)
|
||||
}
|
||||
|
||||
func findLocalSDKDocPath(platform string) string {
|
||||
filename := strings.ToLower(strings.TrimSpace(platform)) + ".md"
|
||||
candidates := []string{
|
||||
filepath.Clean(Tea.Root + "/edge-admin/web/views/@default/httpdns/apps/docs/" + filename),
|
||||
filepath.Clean(Tea.Root + "/EdgeAdmin/web/views/@default/httpdns/apps/docs/" + filename),
|
||||
}
|
||||
return findFirstExistingFile(candidates)
|
||||
return findNewestSDKUploadFileByMeta(platform, "doc")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -14,6 +15,8 @@ import (
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
)
|
||||
|
||||
const sdkUploadMaxFileSize = 20 * 1024 * 1024 // 20MB
|
||||
|
||||
type SdkUploadAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
@@ -52,7 +55,7 @@ func (this *SdkUploadAction) RunPost(params struct {
|
||||
}) {
|
||||
params.Must.Field("appId", params.AppId).Gt(0, "请选择应用")
|
||||
|
||||
platform, _, _, downloadFilename, err := resolveSDKPlatform(params.Platform)
|
||||
platform, _, _, _, err := resolveSDKPlatform(params.Platform)
|
||||
if err != nil {
|
||||
this.Fail(err.Error())
|
||||
return
|
||||
@@ -70,57 +73,21 @@ func (this *SdkUploadAction) RunPost(params struct {
|
||||
}
|
||||
|
||||
uploadDir := sdkUploadDir()
|
||||
err = os.MkdirAll(uploadDir, 0755)
|
||||
if err != nil {
|
||||
if err = os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
this.Fail("创建上传目录失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if params.SdkFile != nil {
|
||||
filename := strings.ToLower(strings.TrimSpace(params.SdkFile.Filename))
|
||||
if !strings.HasSuffix(filename, ".zip") {
|
||||
this.Fail("SDK 包仅支持 .zip 文件")
|
||||
return
|
||||
}
|
||||
|
||||
sdkData, readErr := params.SdkFile.Read()
|
||||
if readErr != nil {
|
||||
this.Fail("读取 SDK 包失败: " + readErr.Error())
|
||||
return
|
||||
}
|
||||
if len(sdkData) == 0 {
|
||||
this.Fail("SDK 包文件为空,请重新上传")
|
||||
return
|
||||
}
|
||||
|
||||
baseName := strings.TrimSuffix(downloadFilename, ".zip")
|
||||
err = saveSDKUploadFile(uploadDir, baseName+"-v"+version+".zip", sdkData)
|
||||
if err != nil {
|
||||
this.Fail("保存 SDK 包失败: " + err.Error())
|
||||
if err = this.saveUploadedItem(uploadDir, platform, version, "sdk", params.SdkFile); err != nil {
|
||||
this.Fail(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if params.DocFile != nil {
|
||||
docName := strings.ToLower(strings.TrimSpace(params.DocFile.Filename))
|
||||
if !strings.HasSuffix(docName, ".md") {
|
||||
this.Fail("集成文档仅支持 .md 文件")
|
||||
return
|
||||
}
|
||||
|
||||
docData, readErr := params.DocFile.Read()
|
||||
if readErr != nil {
|
||||
this.Fail("读取集成文档失败: " + readErr.Error())
|
||||
return
|
||||
}
|
||||
if len(docData) == 0 {
|
||||
this.Fail("集成文档文件为空,请重新上传")
|
||||
return
|
||||
}
|
||||
|
||||
err = saveSDKUploadFile(uploadDir, "httpdns-sdk-"+platform+"-v"+version+".md", docData)
|
||||
if err != nil {
|
||||
this.Fail("保存集成文档失败: " + err.Error())
|
||||
if err = this.saveUploadedItem(uploadDir, platform, version, "doc", params.DocFile); err != nil {
|
||||
this.Fail(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -128,6 +95,52 @@ func (this *SdkUploadAction) RunPost(params struct {
|
||||
this.Success()
|
||||
}
|
||||
|
||||
func (this *SdkUploadAction) saveUploadedItem(uploadDir string, platform string, version string, fileType string, file *actions.File) error {
|
||||
expectedExt := ".md"
|
||||
displayType := "集成文档"
|
||||
if fileType == "sdk" {
|
||||
expectedExt = ".zip"
|
||||
displayType = "SDK 包"
|
||||
}
|
||||
|
||||
filename, err := normalizeUploadedFilename(file.Filename, expectedExt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if file.Size > sdkUploadMaxFileSize {
|
||||
return errors.New(displayType + "文件不能超过 20MB")
|
||||
}
|
||||
|
||||
data, err := file.Read()
|
||||
if err != nil {
|
||||
return errors.New("读取" + displayType + "失败: " + err.Error())
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return errors.New(displayType + "文件为空,请重新上传")
|
||||
}
|
||||
if len(data) > sdkUploadMaxFileSize {
|
||||
return errors.New(displayType + "文件不能超过 20MB")
|
||||
}
|
||||
|
||||
if err = saveSDKUploadFile(uploadDir, filename, data); err != nil {
|
||||
return errors.New("保存" + displayType + "失败: " + err.Error())
|
||||
}
|
||||
|
||||
err = saveSDKUploadMetaRecord(uploadDir, sdkUploadMeta{
|
||||
Platform: platform,
|
||||
Version: version,
|
||||
FileType: fileType,
|
||||
Filename: filename,
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.New("保存上传元信息失败: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeSDKVersion(version string) (string, error) {
|
||||
version = strings.TrimSpace(version)
|
||||
if len(version) == 0 {
|
||||
@@ -142,6 +155,26 @@ func normalizeSDKVersion(version string) (string, error) {
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func normalizeUploadedFilename(raw string, expectedExt string) (string, error) {
|
||||
filename := filepath.Base(strings.TrimSpace(raw))
|
||||
if len(filename) == 0 || filename == "." || filename == string(filepath.Separator) {
|
||||
return "", errors.New("文件名不能为空")
|
||||
}
|
||||
if strings.Contains(filename, "/") || strings.Contains(filename, "\\") || strings.Contains(filename, "..") {
|
||||
return "", errors.New("文件名不合法")
|
||||
}
|
||||
|
||||
actualExt := strings.ToLower(filepath.Ext(filename))
|
||||
if actualExt != strings.ToLower(expectedExt) {
|
||||
if expectedExt == ".zip" {
|
||||
return "", errors.New("SDK 包仅支持 .zip 文件")
|
||||
}
|
||||
return "", errors.New("集成文档仅支持 .md 文件")
|
||||
}
|
||||
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
func saveSDKUploadFile(baseDir string, filename string, data []byte) error {
|
||||
targetPath := filepath.Join(baseDir, filename)
|
||||
tmpPath := targetPath + ".tmp"
|
||||
@@ -152,6 +185,35 @@ func saveSDKUploadFile(baseDir string, filename string, data []byte) error {
|
||||
return os.Rename(tmpPath, targetPath)
|
||||
}
|
||||
|
||||
func saveSDKUploadMetaRecord(baseDir string, meta sdkUploadMeta) error {
|
||||
meta.Platform = strings.ToLower(strings.TrimSpace(meta.Platform))
|
||||
meta.Version = strings.TrimSpace(meta.Version)
|
||||
meta.FileType = strings.ToLower(strings.TrimSpace(meta.FileType))
|
||||
meta.Filename = filepath.Base(strings.TrimSpace(meta.Filename))
|
||||
if len(meta.Platform) == 0 || len(meta.Version) == 0 || len(meta.FileType) == 0 || len(meta.Filename) == 0 {
|
||||
return errors.New("上传元信息不完整")
|
||||
}
|
||||
|
||||
metaFilename := sdkUploadMetaFilename(meta.Platform, meta.Version, meta.FileType)
|
||||
metaPath := filepath.Join(baseDir, metaFilename)
|
||||
if data, err := os.ReadFile(metaPath); err == nil && len(data) > 0 {
|
||||
var oldMeta sdkUploadMeta
|
||||
if json.Unmarshal(data, &oldMeta) == nil {
|
||||
oldFile := filepath.Base(strings.TrimSpace(oldMeta.Filename))
|
||||
if len(oldFile) > 0 && oldFile != meta.Filename {
|
||||
_ = os.Remove(filepath.Join(baseDir, oldFile))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return saveSDKUploadFile(baseDir, metaFilename, data)
|
||||
}
|
||||
|
||||
func listUploadedSDKFiles() []map[string]interface{} {
|
||||
type item struct {
|
||||
Name string
|
||||
@@ -162,45 +224,26 @@ func listUploadedSDKFiles() []map[string]interface{} {
|
||||
UpdatedAt int64
|
||||
}
|
||||
|
||||
byName := map[string]item{}
|
||||
for _, dir := range sdkUploadSearchDirs() {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
items := make([]item, 0)
|
||||
for _, record := range listSDKUploadMetaRecords() {
|
||||
stat, err := os.Stat(record.FilePath)
|
||||
if err != nil || stat.IsDir() {
|
||||
continue
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
platform, version, fileType, ok := parseSDKUploadFilename(name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
info, statErr := entry.Info()
|
||||
if statErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
current := item{
|
||||
Name: name,
|
||||
Platform: platform,
|
||||
FileType: fileType,
|
||||
Version: version,
|
||||
SizeBytes: info.Size(),
|
||||
UpdatedAt: info.ModTime().Unix(),
|
||||
}
|
||||
old, exists := byName[name]
|
||||
if !exists || current.UpdatedAt >= old.UpdatedAt {
|
||||
byName[name] = current
|
||||
}
|
||||
fileType := "SDK 包"
|
||||
if record.Meta.FileType == "doc" {
|
||||
fileType = "集成文档"
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]item, 0, len(byName))
|
||||
for _, it := range byName {
|
||||
items = append(items, it)
|
||||
items = append(items, item{
|
||||
Name: filepath.Base(record.FilePath),
|
||||
Platform: record.Meta.Platform,
|
||||
FileType: fileType,
|
||||
Version: record.Meta.Version,
|
||||
SizeBytes: stat.Size(),
|
||||
UpdatedAt: stat.ModTime().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
@@ -224,55 +267,21 @@ func listUploadedSDKFiles() []map[string]interface{} {
|
||||
return result
|
||||
}
|
||||
|
||||
func parseSDKUploadFilename(filename string) (platform string, version string, fileType string, ok bool) {
|
||||
if !strings.HasPrefix(filename, "httpdns-sdk-") {
|
||||
return "", "", "", false
|
||||
}
|
||||
|
||||
ext := ""
|
||||
switch {
|
||||
case strings.HasSuffix(filename, ".zip"):
|
||||
ext = ".zip"
|
||||
fileType = "SDK包"
|
||||
case strings.HasSuffix(filename, ".md"):
|
||||
ext = ".md"
|
||||
fileType = "集成文档"
|
||||
default:
|
||||
return "", "", "", false
|
||||
}
|
||||
|
||||
main := strings.TrimSuffix(strings.TrimPrefix(filename, "httpdns-sdk-"), ext)
|
||||
version = ""
|
||||
if idx := strings.Index(main, "-v"); idx > 0 && idx+2 < len(main) {
|
||||
version = main[idx+2:]
|
||||
main = main[:idx]
|
||||
}
|
||||
|
||||
main = strings.ToLower(strings.TrimSpace(main))
|
||||
switch main {
|
||||
case "android", "ios", "flutter":
|
||||
platform = main
|
||||
if len(version) == 0 {
|
||||
version = "-"
|
||||
}
|
||||
return platform, version, fileType, true
|
||||
default:
|
||||
return "", "", "", false
|
||||
}
|
||||
}
|
||||
|
||||
func formatSDKFileSize(size int64) string {
|
||||
if size < 1024 {
|
||||
return strconv.FormatInt(size, 10) + " B"
|
||||
}
|
||||
|
||||
sizeKB := float64(size) / 1024
|
||||
if sizeKB < 1024 {
|
||||
return strconv.FormatFloat(sizeKB, 'f', 1, 64) + " KB"
|
||||
}
|
||||
|
||||
sizeMB := sizeKB / 1024
|
||||
if sizeMB < 1024 {
|
||||
return strconv.FormatFloat(sizeMB, 'f', 1, 64) + " MB"
|
||||
}
|
||||
|
||||
sizeGB := sizeMB / 1024
|
||||
return strconv.FormatFloat(sizeGB, 'f', 1, 64) + " GB"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -34,19 +35,16 @@ func (this *SdkUploadDeleteAction) RunPost(params struct {
|
||||
this.Fail("文件名不合法")
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(filename, "httpdns-sdk-") {
|
||||
this.Fail("不允许删除该文件")
|
||||
return
|
||||
}
|
||||
if !(strings.HasSuffix(filename, ".zip") || strings.HasSuffix(filename, ".md")) {
|
||||
this.Fail("不允许删除该文件")
|
||||
lowName := strings.ToLower(filename)
|
||||
if !strings.HasSuffix(lowName, ".zip") && !strings.HasSuffix(lowName, ".md") {
|
||||
this.Fail("仅允许删除 .zip 或 .md 文件")
|
||||
return
|
||||
}
|
||||
|
||||
for _, dir := range sdkUploadDirs() {
|
||||
fullPath := filepath.Join(dir, filename)
|
||||
_, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
stat, err := os.Stat(fullPath)
|
||||
if err != nil || stat.IsDir() {
|
||||
continue
|
||||
}
|
||||
if err = os.Remove(fullPath); err != nil {
|
||||
@@ -55,5 +53,38 @@ func (this *SdkUploadDeleteAction) RunPost(params struct {
|
||||
}
|
||||
}
|
||||
|
||||
// 删除引用该文件的元数据
|
||||
for _, dir := range sdkUploadDirs() {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
if !isSDKUploadMetaFile(entry.Name()) {
|
||||
continue
|
||||
}
|
||||
|
||||
metaPath := filepath.Join(dir, entry.Name())
|
||||
data, err := os.ReadFile(metaPath)
|
||||
if err != nil || len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var meta sdkUploadMeta
|
||||
if err = json.Unmarshal(data, &meta); err != nil {
|
||||
continue
|
||||
}
|
||||
if filepath.Base(strings.TrimSpace(meta.Filename)) != filename {
|
||||
continue
|
||||
}
|
||||
|
||||
_ = os.Remove(metaPath)
|
||||
}
|
||||
}
|
||||
|
||||
this.Success()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||
)
|
||||
|
||||
@@ -50,7 +52,22 @@ func (this *IndexAction) RunGet(params struct {
|
||||
this.Data["nodeDatetime"] = nodeDatetime
|
||||
this.Data["nodeTimeDiff"] = nodeTimeDiff
|
||||
|
||||
this.Data["shouldUpgrade"] = false
|
||||
this.Data["newVersion"] = ""
|
||||
osName := strings.TrimSpace(status.GetString("os"))
|
||||
if len(osName) > 0 {
|
||||
checkVersionResp, err := this.RPC().HTTPDNSNodeRPC().CheckHTTPDNSNodeLatestVersion(this.AdminContext(), &pb.CheckHTTPDNSNodeLatestVersionRequest{
|
||||
Os: osName,
|
||||
Arch: strings.TrimSpace(status.GetString("arch")),
|
||||
CurrentVersion: strings.TrimSpace(status.GetString("buildVersion")),
|
||||
})
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
this.Data["shouldUpgrade"] = checkVersionResp.GetHasNewVersion()
|
||||
this.Data["newVersion"] = checkVersionResp.GetNewVersion()
|
||||
} else {
|
||||
this.Data["shouldUpgrade"] = false
|
||||
this.Data["newVersion"] = ""
|
||||
}
|
||||
this.Show()
|
||||
}
|
||||
|
||||
@@ -22,14 +22,14 @@ func findHTTPDNSClusterMap(parent *actionutils.ParentAction, clusterID int64) (m
|
||||
return maps.Map{
|
||||
"id": clusterID,
|
||||
"name": "",
|
||||
"installDir": "/opt/edge-httpdns",
|
||||
"installDir": "/root/edge-httpdns",
|
||||
}, nil
|
||||
}
|
||||
|
||||
cluster := resp.GetCluster()
|
||||
installDir := strings.TrimSpace(cluster.GetInstallDir())
|
||||
if len(installDir) == 0 {
|
||||
installDir = "/opt/edge-httpdns"
|
||||
installDir = "/root/edge-httpdns"
|
||||
}
|
||||
return maps.Map{
|
||||
"id": cluster.GetId(),
|
||||
@@ -93,7 +93,7 @@ func findHTTPDNSNodeMap(parent *actionutils.ParentAction, nodeID int64) (maps.Ma
|
||||
|
||||
installDir := strings.TrimSpace(node.GetInstallDir())
|
||||
if len(installDir) == 0 {
|
||||
installDir = "/opt/edge-httpdns"
|
||||
installDir = "/root/edge-httpdns"
|
||||
}
|
||||
|
||||
clusterMap, err := findHTTPDNSClusterMap(parent, node.GetClusterId())
|
||||
@@ -137,22 +137,22 @@ func findHTTPDNSNodeMap(parent *actionutils.ParentAction, nodeID int64) (maps.Ma
|
||||
}
|
||||
|
||||
return maps.Map{
|
||||
"id": node.GetId(),
|
||||
"clusterId": node.GetClusterId(),
|
||||
"name": node.GetName(),
|
||||
"isOn": node.GetIsOn(),
|
||||
"isUp": node.GetIsUp(),
|
||||
"isInstalled": node.GetIsInstalled(),
|
||||
"isActive": node.GetIsActive(),
|
||||
"uniqueId": node.GetUniqueId(),
|
||||
"secret": node.GetSecret(),
|
||||
"installDir": installDir,
|
||||
"status": statusMap,
|
||||
"id": node.GetId(),
|
||||
"clusterId": node.GetClusterId(),
|
||||
"name": node.GetName(),
|
||||
"isOn": node.GetIsOn(),
|
||||
"isUp": node.GetIsUp(),
|
||||
"isInstalled": node.GetIsInstalled(),
|
||||
"isActive": node.GetIsActive(),
|
||||
"uniqueId": node.GetUniqueId(),
|
||||
"secret": node.GetSecret(),
|
||||
"installDir": installDir,
|
||||
"status": statusMap,
|
||||
"installStatus": installStatusMap,
|
||||
"cluster": clusterMap,
|
||||
"login": loginMap,
|
||||
"apiNodeAddrs": []string{},
|
||||
"ipAddresses": ipAddresses,
|
||||
"cluster": clusterMap,
|
||||
"login": loginMap,
|
||||
"apiNodeAddrs": []string{},
|
||||
"ipAddresses": ipAddresses,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -165,21 +165,23 @@ func decodeNodeStatus(raw []byte) maps.Map {
|
||||
memText := fmt.Sprintf("%.2f%%", status.MemoryUsage*100)
|
||||
|
||||
return maps.Map{
|
||||
"isActive": status.IsActive,
|
||||
"updatedAt": status.UpdatedAt,
|
||||
"hostname": status.Hostname,
|
||||
"hostIP": status.HostIP,
|
||||
"cpuUsage": status.CPUUsage,
|
||||
"cpuUsageText": cpuText,
|
||||
"memUsage": status.MemoryUsage,
|
||||
"memUsageText": memText,
|
||||
"load1m": status.Load1m,
|
||||
"load5m": status.Load5m,
|
||||
"load15m": status.Load15m,
|
||||
"buildVersion": status.BuildVersion,
|
||||
"cpuPhysicalCount": status.CPUPhysicalCount,
|
||||
"cpuLogicalCount": status.CPULogicalCount,
|
||||
"exePath": status.ExePath,
|
||||
"isActive": status.IsActive,
|
||||
"updatedAt": status.UpdatedAt,
|
||||
"os": status.OS,
|
||||
"arch": status.Arch,
|
||||
"hostname": status.Hostname,
|
||||
"hostIP": status.HostIP,
|
||||
"cpuUsage": status.CPUUsage,
|
||||
"cpuUsageText": cpuText,
|
||||
"memUsage": status.MemoryUsage,
|
||||
"memUsageText": memText,
|
||||
"load1m": status.Load1m,
|
||||
"load5m": status.Load5m,
|
||||
"load15m": status.Load15m,
|
||||
"buildVersion": status.BuildVersion,
|
||||
"cpuPhysicalCount": status.CPUPhysicalCount,
|
||||
"cpuLogicalCount": status.CPULogicalCount,
|
||||
"exePath": status.ExePath,
|
||||
"apiSuccessPercent": status.APISuccessPercent,
|
||||
"apiAvgCostSeconds": status.APIAvgCostSeconds,
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ func (this *UpdateAction) RunPost(params struct {
|
||||
|
||||
installDir := strings.TrimSpace(node.GetInstallDir())
|
||||
if len(installDir) == 0 {
|
||||
installDir = "/opt/edge-httpdns"
|
||||
installDir = "/root/edge-httpdns"
|
||||
}
|
||||
|
||||
_, err = this.RPC().HTTPDNSNodeRPC().UpdateHTTPDNSNode(this.AdminContext(), &pb.UpdateHTTPDNSNodeRequest{
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package clusters
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
type ClusterSettingsAction struct {
|
||||
@@ -41,14 +44,15 @@ func (this *ClusterSettingsAction) RunGet(params struct {
|
||||
}
|
||||
|
||||
settings := maps.Map{
|
||||
"name": cluster.GetString("name"),
|
||||
"gatewayDomain": cluster.GetString("gatewayDomain"),
|
||||
"cacheTtl": cluster.GetInt("defaultTTL"),
|
||||
"fallbackTimeout": cluster.GetInt("fallbackTimeout"),
|
||||
"installDir": cluster.GetString("installDir"),
|
||||
"isOn": cluster.GetBool("isOn"),
|
||||
"autoRemoteStart": cluster.GetBool("autoRemoteStart"),
|
||||
"accessLogIsOn": cluster.GetBool("accessLogIsOn"),
|
||||
"name": cluster.GetString("name"),
|
||||
"gatewayDomain": cluster.GetString("gatewayDomain"),
|
||||
"cacheTtl": cluster.GetInt("defaultTTL"),
|
||||
"fallbackTimeout": cluster.GetInt("fallbackTimeout"),
|
||||
"installDir": cluster.GetString("installDir"),
|
||||
"isOn": cluster.GetBool("isOn"),
|
||||
"autoRemoteStart": cluster.GetBool("autoRemoteStart"),
|
||||
"accessLogIsOn": cluster.GetBool("accessLogIsOn"),
|
||||
"timeZone": cluster.GetString("timeZone"),
|
||||
}
|
||||
if settings.GetInt("cacheTtl") <= 0 {
|
||||
settings["cacheTtl"] = 60
|
||||
@@ -57,7 +61,10 @@ func (this *ClusterSettingsAction) RunGet(params struct {
|
||||
settings["fallbackTimeout"] = 300
|
||||
}
|
||||
if len(settings.GetString("installDir")) == 0 {
|
||||
settings["installDir"] = "/opt/edge-httpdns"
|
||||
settings["installDir"] = "/root/edge-httpdns"
|
||||
}
|
||||
if len(settings.GetString("timeZone")) == 0 {
|
||||
settings["timeZone"] = "Asia/Shanghai"
|
||||
}
|
||||
|
||||
listenAddresses := []*serverconfigs.NetworkAddressConfig{
|
||||
@@ -101,19 +108,29 @@ func (this *ClusterSettingsAction) RunGet(params struct {
|
||||
"listen": listenAddresses,
|
||||
"sslPolicy": sslPolicy,
|
||||
}
|
||||
|
||||
this.Data["timeZoneGroups"] = nodeconfigs.FindAllTimeZoneGroups()
|
||||
this.Data["timeZoneLocations"] = nodeconfigs.FindAllTimeZoneLocations()
|
||||
timeZoneStr := settings.GetString("timeZone")
|
||||
if len(timeZoneStr) == 0 {
|
||||
timeZoneStr = nodeconfigs.DefaultTimeZoneLocation
|
||||
}
|
||||
this.Data["timeZoneLocation"] = nodeconfigs.FindTimeZoneLocation(timeZoneStr)
|
||||
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *ClusterSettingsAction) RunPost(params struct {
|
||||
ClusterId int64
|
||||
Name string
|
||||
GatewayDomain string
|
||||
CacheTtl int32
|
||||
FallbackTimeout int32
|
||||
InstallDir string
|
||||
IsOn bool
|
||||
ClusterId int64
|
||||
Name string
|
||||
GatewayDomain string
|
||||
CacheTtl int32
|
||||
FallbackTimeout int32
|
||||
InstallDir string
|
||||
IsOn bool
|
||||
AutoRemoteStart bool
|
||||
AccessLogIsOn bool
|
||||
AccessLogIsOn bool
|
||||
TimeZone string
|
||||
|
||||
Addresses []byte
|
||||
SslPolicyJSON []byte
|
||||
@@ -129,6 +146,31 @@ func (this *ClusterSettingsAction) RunPost(params struct {
|
||||
params.Must.Field("name", params.Name).Require("请输入集群名称")
|
||||
params.Must.Field("gatewayDomain", params.GatewayDomain).Require("请输入服务域名")
|
||||
|
||||
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 开关项按请求值强制覆盖:未提交/空值都视为 false,支持取消勾选
|
||||
autoRemoteStartRaw := strings.ToLower(strings.TrimSpace(this.ParamString("autoRemoteStart")))
|
||||
params.AutoRemoteStart = autoRemoteStartRaw == "1" || autoRemoteStartRaw == "true" || autoRemoteStartRaw == "on" || autoRemoteStartRaw == "yes" || autoRemoteStartRaw == "enabled"
|
||||
|
||||
accessLogIsOnRaw := strings.ToLower(strings.TrimSpace(this.ParamString("accessLogIsOn")))
|
||||
params.AccessLogIsOn = accessLogIsOnRaw == "1" || accessLogIsOnRaw == "true" || accessLogIsOnRaw == "on" || accessLogIsOnRaw == "yes" || accessLogIsOnRaw == "enabled"
|
||||
|
||||
isOnRaw := strings.ToLower(strings.TrimSpace(this.ParamString("isOn")))
|
||||
params.IsOn = isOnRaw == "1" || isOnRaw == "true" || isOnRaw == "on" || isOnRaw == "yes" || isOnRaw == "enabled"
|
||||
|
||||
// 时区为空时继承当前值,再兜底默认值
|
||||
params.TimeZone = strings.TrimSpace(this.ParamString("timeZone"))
|
||||
if len(params.TimeZone) == 0 {
|
||||
params.TimeZone = strings.TrimSpace(cluster.GetString("timeZone"))
|
||||
}
|
||||
if len(params.TimeZone) == 0 {
|
||||
params.TimeZone = "Asia/Shanghai"
|
||||
}
|
||||
|
||||
if params.CacheTtl <= 0 {
|
||||
params.CacheTtl = 60
|
||||
}
|
||||
@@ -136,20 +178,13 @@ func (this *ClusterSettingsAction) RunPost(params struct {
|
||||
params.FallbackTimeout = 300
|
||||
}
|
||||
if len(params.InstallDir) == 0 {
|
||||
params.InstallDir = "/opt/edge-httpdns"
|
||||
}
|
||||
|
||||
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
params.InstallDir = "/root/edge-httpdns"
|
||||
}
|
||||
|
||||
tlsConfig := maps.Map{}
|
||||
if rawTLS := strings.TrimSpace(cluster.GetString("tlsPolicyJSON")); len(rawTLS) > 0 {
|
||||
_ = json.Unmarshal([]byte(rawTLS), &tlsConfig)
|
||||
}
|
||||
|
||||
if len(params.Addresses) > 0 {
|
||||
var addresses []*serverconfigs.NetworkAddressConfig
|
||||
if err := json.Unmarshal(params.Addresses, &addresses); err != nil {
|
||||
@@ -158,7 +193,6 @@ func (this *ClusterSettingsAction) RunPost(params struct {
|
||||
}
|
||||
tlsConfig["listen"] = addresses
|
||||
}
|
||||
|
||||
if len(params.SslPolicyJSON) > 0 {
|
||||
sslPolicy := &sslconfigs.SSLPolicy{}
|
||||
if err := json.Unmarshal(params.SslPolicyJSON, sslPolicy); err != nil {
|
||||
@@ -177,7 +211,7 @@ func (this *ClusterSettingsAction) RunPost(params struct {
|
||||
}
|
||||
}
|
||||
|
||||
_, err = this.RPC().HTTPDNSClusterRPC().UpdateHTTPDNSCluster(this.AdminContext(), &pb.UpdateHTTPDNSClusterRequest{
|
||||
updateReq := &pb.UpdateHTTPDNSClusterRequest{
|
||||
ClusterId: params.ClusterId,
|
||||
Name: params.Name,
|
||||
ServiceDomain: params.GatewayDomain,
|
||||
@@ -189,7 +223,16 @@ func (this *ClusterSettingsAction) RunPost(params struct {
|
||||
IsDefault: false,
|
||||
AutoRemoteStart: params.AutoRemoteStart,
|
||||
AccessLogIsOn: params.AccessLogIsOn,
|
||||
})
|
||||
TimeZone: params.TimeZone,
|
||||
}
|
||||
updateCtx := metadata.AppendToOutgoingContext(
|
||||
this.AdminContext(),
|
||||
"x-httpdns-auto-remote-start", fmt.Sprintf("%t", updateReq.GetAutoRemoteStart()),
|
||||
"x-httpdns-access-log-is-on", fmt.Sprintf("%t", updateReq.GetAccessLogIsOn()),
|
||||
"x-httpdns-time-zone", updateReq.GetTimeZone(),
|
||||
)
|
||||
|
||||
_, err = this.RPC().HTTPDNSClusterRPC().UpdateHTTPDNSCluster(updateCtx, updateReq)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
@@ -36,7 +37,7 @@ func (this *CreateAction) RunPost(params struct {
|
||||
params.GatewayDomain = strings.TrimSpace(params.GatewayDomain)
|
||||
params.InstallDir = strings.TrimSpace(params.InstallDir)
|
||||
if len(params.InstallDir) == 0 {
|
||||
params.InstallDir = "/opt/edge-httpdns"
|
||||
params.InstallDir = "/root/edge-httpdns"
|
||||
}
|
||||
if params.CacheTtl <= 0 {
|
||||
params.CacheTtl = 60
|
||||
@@ -56,6 +57,9 @@ func (this *CreateAction) RunPost(params struct {
|
||||
InstallDir: params.InstallDir,
|
||||
IsOn: params.IsOn,
|
||||
IsDefault: false,
|
||||
AutoRemoteStart: true,
|
||||
AccessLogIsOn: true,
|
||||
TimeZone: "Asia/Shanghai",
|
||||
})
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
@@ -63,5 +67,12 @@ func (this *CreateAction) RunPost(params struct {
|
||||
}
|
||||
|
||||
this.Data["clusterId"] = resp.GetClusterId()
|
||||
|
||||
// fallback: if frontend JS doesn't intercept form submit, redirect instead of showing raw JSON
|
||||
if len(this.Request.Header.Get("X-Requested-With")) == 0 {
|
||||
this.RedirectURL("/httpdns/clusters/cluster?clusterId=" + strconv.FormatInt(resp.GetClusterId(), 10))
|
||||
return
|
||||
}
|
||||
|
||||
this.Success()
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (this *CreateNodeAction) RunPost(params struct {
|
||||
params.InstallDir = strings.TrimSpace(cluster.GetString("installDir"))
|
||||
}
|
||||
if len(params.InstallDir) == 0 {
|
||||
params.InstallDir = "/opt/edge-httpdns"
|
||||
params.InstallDir = "/root/edge-httpdns"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ func init() {
|
||||
// Node level
|
||||
GetPost("/createNode", new(CreateNodeAction)).
|
||||
Post("/deleteNode", new(DeleteNodeAction)).
|
||||
Get("/upgradeRemote", new(UpgradeRemoteAction)).
|
||||
GetPost("/cluster/upgradeRemote", new(UpgradeRemoteAction)).
|
||||
GetPost("/upgradeRemote", new(UpgradeRemoteAction)).
|
||||
Post("/upgradeStatus", new(UpgradeStatusAction)).
|
||||
GetPost("/updateNodeSSH", new(UpdateNodeSSHAction)).
|
||||
Post("/checkPorts", new(CheckPortsAction)).
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func listClusterMaps(parent *actionutils.ParentAction, keyword string) ([]maps.Map, error) {
|
||||
@@ -37,6 +41,11 @@ func listClusterMaps(parent *actionutils.ParentAction, keyword string) ([]maps.M
|
||||
}
|
||||
}
|
||||
|
||||
countUpgradeNodes, err := countUpgradeHTTPDNSNodes(parent, cluster.GetId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
port := "443"
|
||||
if rawTLS := cluster.GetTlsPolicyJSON(); len(rawTLS) > 0 {
|
||||
tlsConfig := maps.Map{}
|
||||
@@ -56,32 +65,87 @@ func listClusterMaps(parent *actionutils.ParentAction, keyword string) ([]maps.M
|
||||
apiAddress := "https://" + cluster.GetServiceDomain() + ":" + port
|
||||
|
||||
result = append(result, maps.Map{
|
||||
"id": cluster.GetId(),
|
||||
"name": cluster.GetName(),
|
||||
"gatewayDomain": cluster.GetServiceDomain(),
|
||||
"apiAddress": apiAddress,
|
||||
"defaultTTL": cluster.GetDefaultTTL(),
|
||||
"fallbackTimeout": cluster.GetFallbackTimeoutMs(),
|
||||
"installDir": cluster.GetInstallDir(),
|
||||
"isOn": cluster.GetIsOn(),
|
||||
"isDefault": cluster.GetIsDefault(),
|
||||
"countAllNodes": countAllNodes,
|
||||
"countActiveNodes": countActiveNodes,
|
||||
"id": cluster.GetId(),
|
||||
"name": cluster.GetName(),
|
||||
"gatewayDomain": cluster.GetServiceDomain(),
|
||||
"apiAddress": apiAddress,
|
||||
"defaultTTL": cluster.GetDefaultTTL(),
|
||||
"fallbackTimeout": cluster.GetFallbackTimeoutMs(),
|
||||
"installDir": cluster.GetInstallDir(),
|
||||
"timeZone": cluster.GetTimeZone(),
|
||||
"isOn": cluster.GetIsOn(),
|
||||
"isDefault": cluster.GetIsDefault(),
|
||||
"autoRemoteStart": cluster.GetAutoRemoteStart(),
|
||||
"accessLogIsOn": cluster.GetAccessLogIsOn(),
|
||||
"tlsPolicyJSON": cluster.GetTlsPolicyJSON(),
|
||||
"countAllNodes": countAllNodes,
|
||||
"countActiveNodes": countActiveNodes,
|
||||
"countUpgradeNodes": countUpgradeNodes,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func countUpgradeHTTPDNSNodes(parent *actionutils.ParentAction, clusterID int64) (int64, error) {
|
||||
countResp, err := parent.RPC().HTTPDNSNodeRPC().CountAllUpgradeHTTPDNSNodesWithClusterId(parent.AdminContext(), &pb.CountAllUpgradeHTTPDNSNodesWithClusterIdRequest{
|
||||
ClusterId: clusterID,
|
||||
})
|
||||
if err == nil {
|
||||
return countResp.GetCount(), nil
|
||||
}
|
||||
|
||||
grpcStatus, ok := status.FromError(err)
|
||||
if !ok || grpcStatus.Code() != codes.Unimplemented {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Compatibility fallback: old edge-api may not implement countAllUpgradeHTTPDNSNodesWithClusterId yet.
|
||||
listResp, listErr := parent.RPC().HTTPDNSNodeRPC().FindAllUpgradeHTTPDNSNodesWithClusterId(parent.AdminContext(), &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest{
|
||||
ClusterId: clusterID,
|
||||
})
|
||||
if listErr == nil {
|
||||
return int64(len(listResp.GetNodes())), nil
|
||||
}
|
||||
|
||||
listStatus, ok := status.FromError(listErr)
|
||||
if ok && listStatus.Code() == codes.Unimplemented {
|
||||
// Compatibility fallback: both methods missing on old edge-api, don't block page rendering.
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return 0, listErr
|
||||
}
|
||||
|
||||
func findClusterMap(parent *actionutils.ParentAction, clusterID int64) (maps.Map, error) {
|
||||
if clusterID > 0 {
|
||||
var headerMD metadata.MD
|
||||
resp, err := parent.RPC().HTTPDNSClusterRPC().FindHTTPDNSCluster(parent.AdminContext(), &pb.FindHTTPDNSClusterRequest{
|
||||
ClusterId: clusterID,
|
||||
})
|
||||
}, grpc.Header(&headerMD))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.GetCluster() != nil {
|
||||
cluster := resp.GetCluster()
|
||||
autoRemoteStart := cluster.GetAutoRemoteStart()
|
||||
accessLogIsOn := cluster.GetAccessLogIsOn()
|
||||
timeZone := cluster.GetTimeZone()
|
||||
|
||||
// Compatibility fallback:
|
||||
// Some deployed admin binaries may decode newly-added protobuf fields incorrectly.
|
||||
// Read values from grpc response headers as a source of truth.
|
||||
if values := headerMD.Get("x-httpdns-auto-remote-start"); len(values) > 0 {
|
||||
autoRemoteStart = parseBoolLike(values[0], autoRemoteStart)
|
||||
}
|
||||
if values := headerMD.Get("x-httpdns-access-log-is-on"); len(values) > 0 {
|
||||
accessLogIsOn = parseBoolLike(values[0], accessLogIsOn)
|
||||
}
|
||||
if values := headerMD.Get("x-httpdns-time-zone"); len(values) > 0 {
|
||||
if tz := strings.TrimSpace(values[0]); len(tz) > 0 {
|
||||
timeZone = tz
|
||||
}
|
||||
}
|
||||
|
||||
return maps.Map{
|
||||
"id": cluster.GetId(),
|
||||
"name": cluster.GetName(),
|
||||
@@ -92,8 +156,9 @@ func findClusterMap(parent *actionutils.ParentAction, clusterID int64) (maps.Map
|
||||
"isOn": cluster.GetIsOn(),
|
||||
"isDefault": cluster.GetIsDefault(),
|
||||
"tlsPolicyJSON": cluster.GetTlsPolicyJSON(),
|
||||
"autoRemoteStart": cluster.GetAutoRemoteStart(),
|
||||
"accessLogIsOn": cluster.GetAccessLogIsOn(),
|
||||
"autoRemoteStart": autoRemoteStart,
|
||||
"accessLogIsOn": accessLogIsOn,
|
||||
"timeZone": timeZone,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -109,12 +174,25 @@ func findClusterMap(parent *actionutils.ParentAction, clusterID int64) (maps.Map
|
||||
"gatewayDomain": "",
|
||||
"defaultTTL": 60,
|
||||
"fallbackTimeout": 300,
|
||||
"installDir": "/opt/edge-httpdns",
|
||||
"installDir": "/root/edge-httpdns",
|
||||
"timeZone": "Asia/Shanghai",
|
||||
}, nil
|
||||
}
|
||||
return clusters[0], nil
|
||||
}
|
||||
|
||||
func parseBoolLike(raw string, defaultValue bool) bool {
|
||||
s := strings.ToLower(strings.TrimSpace(raw))
|
||||
switch s {
|
||||
case "1", "true", "on", "yes", "enabled":
|
||||
return true
|
||||
case "0", "false", "off", "no", "disabled":
|
||||
return false
|
||||
default:
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
func listNodeMaps(parent *actionutils.ParentAction, clusterID int64) ([]maps.Map, error) {
|
||||
resp, err := parent.RPC().HTTPDNSNodeRPC().ListHTTPDNSNodes(parent.AdminContext(), &pb.ListHTTPDNSNodesRequest{
|
||||
ClusterId: clusterID,
|
||||
@@ -132,21 +210,21 @@ func listNodeMaps(parent *actionutils.ParentAction, clusterID int64) ([]maps.Map
|
||||
ip = parsed
|
||||
}
|
||||
nodeMap := maps.Map{
|
||||
"id": node.GetId(),
|
||||
"clusterId": node.GetClusterId(),
|
||||
"name": node.GetName(),
|
||||
"isOn": node.GetIsOn(),
|
||||
"isUp": node.GetIsUp(),
|
||||
"isInstalled": node.GetIsInstalled(),
|
||||
"isActive": node.GetIsActive(),
|
||||
"installDir": node.GetInstallDir(),
|
||||
"uniqueId": node.GetUniqueId(),
|
||||
"secret": node.GetSecret(),
|
||||
"status": statusMap,
|
||||
"id": node.GetId(),
|
||||
"clusterId": node.GetClusterId(),
|
||||
"name": node.GetName(),
|
||||
"isOn": node.GetIsOn(),
|
||||
"isUp": node.GetIsUp(),
|
||||
"isInstalled": node.GetIsInstalled(),
|
||||
"isActive": node.GetIsActive(),
|
||||
"installDir": node.GetInstallDir(),
|
||||
"uniqueId": node.GetUniqueId(),
|
||||
"secret": node.GetSecret(),
|
||||
"status": statusMap,
|
||||
"installStatus": installStatusMap,
|
||||
"region": nil,
|
||||
"login": nil,
|
||||
"apiNodeAddrs": []string{},
|
||||
"region": nil,
|
||||
"login": nil,
|
||||
"apiNodeAddrs": []string{},
|
||||
"cluster": maps.Map{
|
||||
"id": node.GetClusterId(),
|
||||
"installDir": node.GetInstallDir(),
|
||||
@@ -227,21 +305,21 @@ func decodeNodeStatus(raw []byte) maps.Map {
|
||||
cpuText := fmt.Sprintf("%.2f%%", status.CPUUsage*100)
|
||||
memText := fmt.Sprintf("%.2f%%", status.MemoryUsage*100)
|
||||
return maps.Map{
|
||||
"isActive": status.IsActive,
|
||||
"updatedAt": status.UpdatedAt,
|
||||
"hostname": status.Hostname,
|
||||
"hostIP": status.HostIP,
|
||||
"cpuUsage": status.CPUUsage,
|
||||
"cpuUsageText": cpuText,
|
||||
"memUsage": status.MemoryUsage,
|
||||
"memUsageText": memText,
|
||||
"load1m": status.Load1m,
|
||||
"load5m": status.Load5m,
|
||||
"load15m": status.Load15m,
|
||||
"buildVersion": status.BuildVersion,
|
||||
"isActive": status.IsActive,
|
||||
"updatedAt": status.UpdatedAt,
|
||||
"hostname": status.Hostname,
|
||||
"hostIP": status.HostIP,
|
||||
"cpuUsage": status.CPUUsage,
|
||||
"cpuUsageText": cpuText,
|
||||
"memUsage": status.MemoryUsage,
|
||||
"memUsageText": memText,
|
||||
"load1m": status.Load1m,
|
||||
"load5m": status.Load5m,
|
||||
"load15m": status.Load15m,
|
||||
"buildVersion": status.BuildVersion,
|
||||
"cpuPhysicalCount": status.CPUPhysicalCount,
|
||||
"cpuLogicalCount": status.CPULogicalCount,
|
||||
"exePath": status.ExePath,
|
||||
"cpuLogicalCount": status.CPULogicalCount,
|
||||
"exePath": status.ExePath,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,108 @@
|
||||
package clusters
|
||||
|
||||
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/httpdns/httpdnsutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type UpgradeRemoteAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *UpgradeRemoteAction) Init() {
|
||||
this.Nav("httpdns", "cluster", "index")
|
||||
}
|
||||
|
||||
func (this *UpgradeRemoteAction) RunGet(params struct {
|
||||
NodeId int64
|
||||
ClusterId int64
|
||||
}) {
|
||||
this.Data["nodeId"] = params.NodeId
|
||||
httpdnsutils.AddLeftMenu(this.Parent())
|
||||
|
||||
cluster, err := findClusterMap(this.Parent(), params.ClusterId)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
httpdnsutils.AddClusterTabbar(this.Parent(), cluster.GetString("name"), params.ClusterId, "node")
|
||||
|
||||
this.Data["clusterId"] = params.ClusterId
|
||||
this.Data["cluster"] = cluster
|
||||
|
||||
resp, err := this.RPC().HTTPDNSNodeRPC().FindAllUpgradeHTTPDNSNodesWithClusterId(this.AdminContext(), &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest{
|
||||
ClusterId: params.ClusterId,
|
||||
})
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
|
||||
nodes := make([]maps.Map, 0, len(resp.GetNodes()))
|
||||
for _, upgradeNode := range resp.GetNodes() {
|
||||
node := upgradeNode.Node
|
||||
if node == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
loginParams := maps.Map{}
|
||||
if node.GetNodeLogin() != nil && len(node.GetNodeLogin().GetParams()) > 0 {
|
||||
_ = json.Unmarshal(node.GetNodeLogin().GetParams(), &loginParams)
|
||||
}
|
||||
|
||||
status := decodeNodeStatus(node.GetStatusJSON())
|
||||
accessIP := strings.TrimSpace(status.GetString("hostIP"))
|
||||
if len(accessIP) == 0 {
|
||||
accessIP = strings.TrimSpace(node.GetName())
|
||||
}
|
||||
|
||||
nodes = append(nodes, maps.Map{
|
||||
"id": node.GetId(),
|
||||
"name": node.GetName(),
|
||||
"accessIP": accessIP,
|
||||
"oldVersion": upgradeNode.OldVersion,
|
||||
"newVersion": upgradeNode.NewVersion,
|
||||
"login": node.GetNodeLogin(),
|
||||
"loginParams": loginParams,
|
||||
"installStatus": decodeUpgradeInstallStatus(node.GetInstallStatusJSON()),
|
||||
})
|
||||
}
|
||||
this.Data["nodes"] = nodes
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *UpgradeRemoteAction) RunPost(params struct {
|
||||
NodeId int64
|
||||
|
||||
Must *actions.Must
|
||||
}) {
|
||||
_, err := this.RPC().HTTPDNSNodeRPC().UpgradeHTTPDNSNode(this.AdminContext(), &pb.UpgradeHTTPDNSNodeRequest{
|
||||
NodeId: params.NodeId,
|
||||
})
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
this.Success()
|
||||
}
|
||||
|
||||
func decodeUpgradeInstallStatus(raw []byte) maps.Map {
|
||||
result := maps.Map{
|
||||
"isRunning": false,
|
||||
"isFinished": false,
|
||||
"isOk": false,
|
||||
"error": "",
|
||||
"errorCode": "",
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
_ = json.Unmarshal(raw, &result)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package clusters
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
)
|
||||
|
||||
type UpgradeStatusAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *UpgradeStatusAction) RunPost(params struct {
|
||||
NodeId int64
|
||||
}) {
|
||||
resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{
|
||||
NodeId: params.NodeId,
|
||||
})
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
if resp.GetNode() == nil {
|
||||
this.Data["status"] = nil
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
this.Data["status"] = decodeUpgradeInstallStatusMap(resp.GetNode().GetInstallStatusJSON())
|
||||
this.Success()
|
||||
}
|
||||
|
||||
func decodeUpgradeInstallStatusMap(raw []byte) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"isRunning": false,
|
||||
"isFinished": false,
|
||||
"isOk": false,
|
||||
"error": "",
|
||||
"errorCode": "",
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
_ = json.Unmarshal(raw, &result)
|
||||
return result
|
||||
}
|
||||
@@ -39,6 +39,7 @@ func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool)
|
||||
if configloaders.AllowModule(adminId, configloaders.AdminModuleCodeSetting) {
|
||||
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabAdminServer), "", "/settings/server", "", this.tab == "server")
|
||||
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabAdminUI), "", "/settings/ui", "", this.tab == "ui")
|
||||
tabbar.Add("升级设置", "", "/settings/upgrade", "", this.tab == "upgrade")
|
||||
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabAdminSecuritySettings), "", "/settings/security", "", this.tab == "security")
|
||||
if teaconst.IsPlus {
|
||||
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabIPLibrary), "", "/settings/ip-library", "", this.tab == "ipLibrary")
|
||||
|
||||
@@ -42,6 +42,7 @@ func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool)
|
||||
if teaconst.IsPlus {
|
||||
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabUserUI), "", "/settings/user-ui", "", this.tab == "userUI")
|
||||
}
|
||||
tabbar.Add("升级设置", "", "/settings/upgrade", "", this.tab == "upgrade")
|
||||
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabAdminSecuritySettings), "", "/settings/security", "", this.tab == "security")
|
||||
tabbar.Add(this.Lang(actionPtr, codes.AdminSetting_TabIPLibrary), "", "/settings/ip-library", "", this.tab == "ipLibrary")
|
||||
}
|
||||
|
||||
@@ -56,9 +56,6 @@ func (this *IndexAction) RunPost(params struct {
|
||||
TimeZone string
|
||||
DnsResolverType string
|
||||
|
||||
SupportModuleCDN bool
|
||||
SupportModuleNS bool
|
||||
|
||||
Must *actions.Must
|
||||
CSRF *actionutils.CSRF
|
||||
}) {
|
||||
@@ -93,13 +90,7 @@ func (this *IndexAction) RunPost(params struct {
|
||||
config.DefaultPageSize = 10
|
||||
}
|
||||
|
||||
config.Modules = []userconfigs.UserModule{}
|
||||
if params.SupportModuleCDN {
|
||||
config.Modules = append(config.Modules, userconfigs.UserModuleCDN)
|
||||
}
|
||||
if params.SupportModuleNS {
|
||||
config.Modules = append(config.Modules, userconfigs.UserModuleNS)
|
||||
}
|
||||
config.Modules = []userconfigs.UserModule{userconfigs.UserModuleCDN, userconfigs.UserModuleNS}
|
||||
|
||||
// 上传Favicon文件
|
||||
if params.FaviconFile != nil {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//go:build !plus
|
||||
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
// loadDNSUpgradeModules 非Plus版本不支持DNS模块
|
||||
func loadDNSUpgradeModules(parent *actionutils.ParentAction) []maps.Map {
|
||||
return nil
|
||||
}
|
||||
|
||||
// upgradeDNSNode 非Plus版本不支持DNS节点升级
|
||||
func upgradeDNSNode(parent *actionutils.ParentAction, nodeId int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadDNSNodeStatus 非Plus版本不支持DNS节点状态查询
|
||||
func loadDNSNodeStatus(parent *actionutils.ParentAction, nodeIds []int64) []maps.Map {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
//go:build plus
|
||||
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
// loadDNSUpgradeModules 加载DNS模块的待升级节点信息
|
||||
func loadDNSUpgradeModules(parent *actionutils.ParentAction) []maps.Map {
|
||||
clustersResp, err := parent.RPC().NSClusterRPC().ListNSClusters(parent.AdminContext(), &pb.ListNSClustersRequest{
|
||||
Offset: 0,
|
||||
Size: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var clusterMaps []maps.Map
|
||||
for _, cluster := range clustersResp.NsClusters {
|
||||
nodesResp, err := parent.RPC().NSNodeRPC().FindAllUpgradeNSNodesWithNSClusterId(parent.AdminContext(), &pb.FindAllUpgradeNSNodesWithNSClusterIdRequest{
|
||||
NsClusterId: cluster.Id,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var nodeMaps []maps.Map
|
||||
for _, nodeUpgrade := range nodesResp.Nodes {
|
||||
if nodeUpgrade.NsNode == nil {
|
||||
continue
|
||||
}
|
||||
installStatusMap := decodeInstallStatusFromPB(nodeUpgrade.NsNode.InstallStatus)
|
||||
accessIP, login, loginParams := decodeNodeAccessInfo(nodeUpgrade.NsNode.StatusJSON, nodeUpgrade.NsNode.NodeLogin, nodeUpgrade.NsNode.Name)
|
||||
|
||||
nodeMaps = append(nodeMaps, maps.Map{
|
||||
"id": nodeUpgrade.NsNode.Id,
|
||||
"name": nodeUpgrade.NsNode.Name,
|
||||
"os": nodeUpgrade.Os,
|
||||
"arch": nodeUpgrade.Arch,
|
||||
"oldVersion": nodeUpgrade.OldVersion,
|
||||
"newVersion": nodeUpgrade.NewVersion,
|
||||
"isOn": nodeUpgrade.NsNode.IsOn,
|
||||
"isUp": nodeUpgrade.NsNode.IsUp,
|
||||
"accessIP": accessIP,
|
||||
"login": login,
|
||||
"loginParams": loginParams,
|
||||
"installStatus": installStatusMap,
|
||||
})
|
||||
}
|
||||
if len(nodeMaps) == 0 {
|
||||
continue
|
||||
}
|
||||
clusterMaps = append(clusterMaps, maps.Map{
|
||||
"id": cluster.Id,
|
||||
"name": cluster.Name,
|
||||
"nodes": nodeMaps,
|
||||
"count": len(nodeMaps),
|
||||
})
|
||||
}
|
||||
return clusterMaps
|
||||
}
|
||||
|
||||
// upgradeDNSNode 升级DNS节点
|
||||
func upgradeDNSNode(parent *actionutils.ParentAction, nodeId int64) error {
|
||||
_, err := parent.RPC().NSNodeRPC().UpgradeNSNode(parent.AdminContext(), &pb.UpgradeNSNodeRequest{
|
||||
NsNodeId: nodeId,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// loadDNSNodeStatus 加载DNS节点安装状态
|
||||
func loadDNSNodeStatus(parent *actionutils.ParentAction, nodeIds []int64) []maps.Map {
|
||||
var result []maps.Map
|
||||
for _, nodeId := range nodeIds {
|
||||
resp, err := parent.RPC().NSNodeRPC().FindNSNode(parent.AdminContext(), &pb.FindNSNodeRequest{
|
||||
NsNodeId: nodeId,
|
||||
})
|
||||
if err != nil || resp.NsNode == nil {
|
||||
continue
|
||||
}
|
||||
var installStatusMap maps.Map
|
||||
if resp.NsNode.InstallStatus != nil {
|
||||
installStatusMap = maps.Map{
|
||||
"isRunning": resp.NsNode.InstallStatus.IsRunning,
|
||||
"isFinished": resp.NsNode.InstallStatus.IsFinished,
|
||||
"isOk": resp.NsNode.InstallStatus.IsOk,
|
||||
"error": resp.NsNode.InstallStatus.Error,
|
||||
"errorCode": resp.NsNode.InstallStatus.ErrorCode,
|
||||
"updatedAt": resp.NsNode.InstallStatus.UpdatedAt,
|
||||
}
|
||||
}
|
||||
result = append(result, maps.Map{
|
||||
"id": nodeId,
|
||||
"installStatus": installStatusMap,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,6 +1,15 @@
|
||||
package upgrade
|
||||
|
||||
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
|
||||
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
@@ -11,5 +20,264 @@ func (this *IndexAction) Init() {
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct{}) {
|
||||
// 加载升级配置
|
||||
config, err := configloaders.LoadUpgradeConfig()
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
this.Data["config"] = config
|
||||
|
||||
// 模块列表
|
||||
modules := []maps.Map{}
|
||||
|
||||
// 1. 边缘节点 (EdgeNode)
|
||||
nodeModule := this.loadEdgeNodeModule()
|
||||
if nodeModule != nil {
|
||||
modules = append(modules, nodeModule)
|
||||
}
|
||||
|
||||
// 2. DNS节点 (EdgeDNS) — 仅Plus版本
|
||||
if teaconst.IsPlus {
|
||||
dnsClusters := loadDNSUpgradeModules(&this.ParentAction)
|
||||
if len(dnsClusters) > 0 {
|
||||
totalCount := 0
|
||||
for _, c := range dnsClusters {
|
||||
totalCount += c.GetInt("count")
|
||||
}
|
||||
modules = append(modules, maps.Map{
|
||||
"name": "DNS节点",
|
||||
"code": "dns",
|
||||
"clusters": dnsClusters,
|
||||
"count": totalCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 3. HTTPDNS节点
|
||||
httpdnsModule := this.loadHTTPDNSModule()
|
||||
if httpdnsModule != nil {
|
||||
modules = append(modules, httpdnsModule)
|
||||
}
|
||||
|
||||
this.Data["modules"] = modules
|
||||
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunPost(params struct {
|
||||
AutoUpgrade bool
|
||||
}) {
|
||||
config, err := configloaders.LoadUpgradeConfig()
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
|
||||
config.AutoUpgrade = params.AutoUpgrade
|
||||
|
||||
err = configloaders.UpdateUpgradeConfig(config)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
|
||||
this.Success()
|
||||
}
|
||||
|
||||
// loadEdgeNodeModule 加载边缘节点模块的待升级信息
|
||||
func (this *IndexAction) loadEdgeNodeModule() maps.Map {
|
||||
// 获取所有集群
|
||||
clustersResp, err := this.RPC().NodeClusterRPC().ListEnabledNodeClusters(this.AdminContext(), &pb.ListEnabledNodeClustersRequest{
|
||||
Offset: 0,
|
||||
Size: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var clusterMaps []maps.Map
|
||||
totalCount := 0
|
||||
for _, cluster := range clustersResp.NodeClusters {
|
||||
resp, err := this.RPC().NodeRPC().FindAllUpgradeNodesWithNodeClusterId(this.AdminContext(), &pb.FindAllUpgradeNodesWithNodeClusterIdRequest{
|
||||
NodeClusterId: cluster.Id,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(resp.Nodes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var nodeMaps []maps.Map
|
||||
for _, nodeUpgrade := range resp.Nodes {
|
||||
if nodeUpgrade.Node == nil {
|
||||
continue
|
||||
}
|
||||
installStatusMap := decodeInstallStatusFromPB(nodeUpgrade.Node.InstallStatus)
|
||||
accessIP, login, loginParams := decodeNodeAccessInfo(nodeUpgrade.Node.StatusJSON, nodeUpgrade.Node.NodeLogin, nodeUpgrade.Node.Name)
|
||||
|
||||
nodeMaps = append(nodeMaps, maps.Map{
|
||||
"id": nodeUpgrade.Node.Id,
|
||||
"name": nodeUpgrade.Node.Name,
|
||||
"os": nodeUpgrade.Os,
|
||||
"arch": nodeUpgrade.Arch,
|
||||
"oldVersion": nodeUpgrade.OldVersion,
|
||||
"newVersion": nodeUpgrade.NewVersion,
|
||||
"isOn": nodeUpgrade.Node.IsOn,
|
||||
"isUp": nodeUpgrade.Node.IsUp,
|
||||
"accessIP": accessIP,
|
||||
"login": login,
|
||||
"loginParams": loginParams,
|
||||
"installStatus": installStatusMap,
|
||||
})
|
||||
}
|
||||
totalCount += len(nodeMaps)
|
||||
clusterMaps = append(clusterMaps, maps.Map{
|
||||
"id": cluster.Id,
|
||||
"name": cluster.Name,
|
||||
"nodes": nodeMaps,
|
||||
"count": len(nodeMaps),
|
||||
})
|
||||
}
|
||||
|
||||
if len(clusterMaps) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return maps.Map{
|
||||
"name": "边缘节点",
|
||||
"code": "node",
|
||||
"clusters": clusterMaps,
|
||||
"count": totalCount,
|
||||
}
|
||||
}
|
||||
|
||||
// loadHTTPDNSModule 加载HTTPDNS模块的待升级信息
|
||||
func (this *IndexAction) loadHTTPDNSModule() maps.Map {
|
||||
clustersResp, err := this.RPC().HTTPDNSClusterRPC().ListHTTPDNSClusters(this.AdminContext(), &pb.ListHTTPDNSClustersRequest{
|
||||
Offset: 0,
|
||||
Size: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var clusterMaps []maps.Map
|
||||
totalCount := 0
|
||||
for _, cluster := range clustersResp.Clusters {
|
||||
resp, err := this.RPC().HTTPDNSNodeRPC().FindAllUpgradeHTTPDNSNodesWithClusterId(this.AdminContext(), &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest{
|
||||
ClusterId: cluster.Id,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(resp.Nodes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var nodeMaps []maps.Map
|
||||
for _, nodeUpgrade := range resp.Nodes {
|
||||
if nodeUpgrade.Node == nil {
|
||||
continue
|
||||
}
|
||||
installStatusMap := decodeInstallStatusFromJSON(nodeUpgrade.Node.InstallStatusJSON)
|
||||
accessIP, login, loginParams := decodeNodeAccessInfo(nodeUpgrade.Node.StatusJSON, nodeUpgrade.Node.NodeLogin, nodeUpgrade.Node.Name)
|
||||
|
||||
nodeMaps = append(nodeMaps, maps.Map{
|
||||
"id": nodeUpgrade.Node.Id,
|
||||
"name": nodeUpgrade.Node.Name,
|
||||
"os": nodeUpgrade.Os,
|
||||
"arch": nodeUpgrade.Arch,
|
||||
"oldVersion": nodeUpgrade.OldVersion,
|
||||
"newVersion": nodeUpgrade.NewVersion,
|
||||
"isOn": nodeUpgrade.Node.IsOn,
|
||||
"isUp": nodeUpgrade.Node.IsUp,
|
||||
"accessIP": accessIP,
|
||||
"login": login,
|
||||
"loginParams": loginParams,
|
||||
"installStatus": installStatusMap,
|
||||
})
|
||||
}
|
||||
totalCount += len(nodeMaps)
|
||||
clusterMaps = append(clusterMaps, maps.Map{
|
||||
"id": cluster.Id,
|
||||
"name": cluster.Name,
|
||||
"nodes": nodeMaps,
|
||||
"count": len(nodeMaps),
|
||||
})
|
||||
}
|
||||
|
||||
if len(clusterMaps) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return maps.Map{
|
||||
"name": "HTTPDNS节点",
|
||||
"code": "httpdns",
|
||||
"clusters": clusterMaps,
|
||||
"count": totalCount,
|
||||
}
|
||||
}
|
||||
|
||||
// decodeInstallStatusFromPB 从 protobuf InstallStatus 解码安装状态
|
||||
func decodeInstallStatusFromPB(status *pb.NodeInstallStatus) maps.Map {
|
||||
if status == nil {
|
||||
return nil
|
||||
}
|
||||
// 历史成功状态,在待升级列表中忽略
|
||||
if status.IsFinished && status.IsOk {
|
||||
return nil
|
||||
}
|
||||
return maps.Map{
|
||||
"isRunning": status.IsRunning,
|
||||
"isFinished": status.IsFinished,
|
||||
"isOk": status.IsOk,
|
||||
"error": status.Error,
|
||||
"errorCode": status.ErrorCode,
|
||||
"updatedAt": status.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// decodeInstallStatusFromJSON 从 JSON 字节解码安装状态
|
||||
func decodeInstallStatusFromJSON(raw []byte) maps.Map {
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := maps.Map{}
|
||||
_ = json.Unmarshal(raw, &result)
|
||||
|
||||
isFinished, _ := result["isFinished"].(bool)
|
||||
isOk, _ := result["isOk"].(bool)
|
||||
if isFinished && isOk {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// decodeNodeAccessInfo 从节点状态和登录信息中提取 accessIP、login、loginParams
|
||||
func decodeNodeAccessInfo(statusJSON []byte, nodeLogin *pb.NodeLogin, nodeName string) (accessIP string, login maps.Map, loginParams maps.Map) {
|
||||
// 从 statusJSON 中提取 hostIP 作为 accessIP
|
||||
if len(statusJSON) > 0 {
|
||||
statusMap := maps.Map{}
|
||||
_ = json.Unmarshal(statusJSON, &statusMap)
|
||||
accessIP = strings.TrimSpace(statusMap.GetString("hostIP"))
|
||||
}
|
||||
if len(accessIP) == 0 {
|
||||
accessIP = strings.TrimSpace(nodeName)
|
||||
}
|
||||
|
||||
// 解码 login 信息
|
||||
if nodeLogin != nil {
|
||||
login = maps.Map{
|
||||
"id": nodeLogin.Id,
|
||||
"name": nodeLogin.Name,
|
||||
"type": nodeLogin.Type,
|
||||
}
|
||||
if len(nodeLogin.Params) > 0 {
|
||||
loginParams = maps.Map{}
|
||||
_ = json.Unmarshal(nodeLogin.Params, &loginParams)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ func init() {
|
||||
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeSetting)).
|
||||
Helper(settingutils.NewHelper("upgrade")).
|
||||
Prefix("/settings/upgrade").
|
||||
Get("", new(IndexAction)).
|
||||
GetPost("", new(IndexAction)).
|
||||
Post("/upgradeNode", new(UpgradeNodeAction)).
|
||||
Post("/status", new(StatusAction)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type StatusAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *StatusAction) RunPost(params struct {
|
||||
NodeIdsJSON []byte // JSON: {"node": [1,2], "dns": [3], "httpdns": [4,5]}
|
||||
}) {
|
||||
var nodeIdsMap map[string][]int64
|
||||
if len(params.NodeIdsJSON) > 0 {
|
||||
_ = json.Unmarshal(params.NodeIdsJSON, &nodeIdsMap)
|
||||
}
|
||||
|
||||
result := maps.Map{}
|
||||
|
||||
// EdgeNode 状态
|
||||
if nodeIds, ok := nodeIdsMap["node"]; ok && len(nodeIds) > 0 {
|
||||
var nodeStatuses []maps.Map
|
||||
for _, nodeId := range nodeIds {
|
||||
resp, err := this.RPC().NodeRPC().FindEnabledNode(this.AdminContext(), &pb.FindEnabledNodeRequest{NodeId: nodeId})
|
||||
if err != nil || resp.Node == nil {
|
||||
continue
|
||||
}
|
||||
var installStatusMap maps.Map
|
||||
if resp.Node.InstallStatus != nil {
|
||||
installStatusMap = maps.Map{
|
||||
"isRunning": resp.Node.InstallStatus.IsRunning,
|
||||
"isFinished": resp.Node.InstallStatus.IsFinished,
|
||||
"isOk": resp.Node.InstallStatus.IsOk,
|
||||
"error": resp.Node.InstallStatus.Error,
|
||||
"errorCode": resp.Node.InstallStatus.ErrorCode,
|
||||
"updatedAt": resp.Node.InstallStatus.UpdatedAt,
|
||||
}
|
||||
}
|
||||
nodeStatuses = append(nodeStatuses, maps.Map{
|
||||
"id": nodeId,
|
||||
"installStatus": installStatusMap,
|
||||
})
|
||||
}
|
||||
result["node"] = nodeStatuses
|
||||
}
|
||||
|
||||
// DNS 状态
|
||||
if nodeIds, ok := nodeIdsMap["dns"]; ok && len(nodeIds) > 0 {
|
||||
result["dns"] = loadDNSNodeStatus(&this.ParentAction, nodeIds)
|
||||
}
|
||||
|
||||
// HTTPDNS 状态
|
||||
if nodeIds, ok := nodeIdsMap["httpdns"]; ok && len(nodeIds) > 0 {
|
||||
var nodeStatuses []maps.Map
|
||||
for _, nodeId := range nodeIds {
|
||||
resp, err := this.RPC().HTTPDNSNodeRPC().FindHTTPDNSNode(this.AdminContext(), &pb.FindHTTPDNSNodeRequest{NodeId: nodeId})
|
||||
if err != nil || resp.Node == nil {
|
||||
continue
|
||||
}
|
||||
var installStatusMap maps.Map
|
||||
if len(resp.Node.InstallStatusJSON) > 0 {
|
||||
installStatusMap = maps.Map{}
|
||||
_ = json.Unmarshal(resp.Node.InstallStatusJSON, &installStatusMap)
|
||||
}
|
||||
nodeStatuses = append(nodeStatuses, maps.Map{
|
||||
"id": nodeId,
|
||||
"installStatus": installStatusMap,
|
||||
})
|
||||
}
|
||||
result["httpdns"] = nodeStatuses
|
||||
}
|
||||
|
||||
this.Data["statuses"] = result
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
)
|
||||
|
||||
type UpgradeNodeAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *UpgradeNodeAction) RunPost(params struct {
|
||||
Module string // node, dns, httpdns
|
||||
Scope string // all, module, cluster, node
|
||||
ClusterId int64
|
||||
NodeId int64
|
||||
}) {
|
||||
switch params.Scope {
|
||||
case "node":
|
||||
err := this.upgradeSingleNode(params.Module, params.NodeId)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
case "cluster":
|
||||
err := this.upgradeCluster(params.Module, params.ClusterId)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
case "module":
|
||||
err := this.upgradeModule(params.Module)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
case "all":
|
||||
_ = this.upgradeModule("node")
|
||||
_ = this.upgradeModule("dns")
|
||||
_ = this.upgradeModule("httpdns")
|
||||
}
|
||||
|
||||
this.Success()
|
||||
}
|
||||
|
||||
func (this *UpgradeNodeAction) upgradeSingleNode(module string, nodeId int64) error {
|
||||
switch module {
|
||||
case "node":
|
||||
_, err := this.RPC().NodeRPC().UpgradeNode(this.AdminContext(), &pb.UpgradeNodeRequest{NodeId: nodeId})
|
||||
return err
|
||||
case "dns":
|
||||
return upgradeDNSNode(&this.ParentAction, nodeId)
|
||||
case "httpdns":
|
||||
_, err := this.RPC().HTTPDNSNodeRPC().UpgradeHTTPDNSNode(this.AdminContext(), &pb.UpgradeHTTPDNSNodeRequest{NodeId: nodeId})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *UpgradeNodeAction) upgradeCluster(module string, clusterId int64) error {
|
||||
switch module {
|
||||
case "node":
|
||||
resp, err := this.RPC().NodeRPC().FindAllUpgradeNodesWithNodeClusterId(this.AdminContext(), &pb.FindAllUpgradeNodesWithNodeClusterIdRequest{
|
||||
NodeClusterId: clusterId,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, nodeUpgrade := range resp.Nodes {
|
||||
if nodeUpgrade.Node == nil {
|
||||
continue
|
||||
}
|
||||
_, _ = this.RPC().NodeRPC().UpgradeNode(this.AdminContext(), &pb.UpgradeNodeRequest{NodeId: nodeUpgrade.Node.Id})
|
||||
}
|
||||
case "dns":
|
||||
this.upgradeDNSCluster(clusterId)
|
||||
case "httpdns":
|
||||
resp, err := this.RPC().HTTPDNSNodeRPC().FindAllUpgradeHTTPDNSNodesWithClusterId(this.AdminContext(), &pb.FindAllUpgradeHTTPDNSNodesWithClusterIdRequest{
|
||||
ClusterId: clusterId,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, nodeUpgrade := range resp.Nodes {
|
||||
if nodeUpgrade.Node == nil {
|
||||
continue
|
||||
}
|
||||
_, _ = this.RPC().HTTPDNSNodeRPC().UpgradeHTTPDNSNode(this.AdminContext(), &pb.UpgradeHTTPDNSNodeRequest{NodeId: nodeUpgrade.Node.Id})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *UpgradeNodeAction) upgradeModule(module string) error {
|
||||
switch module {
|
||||
case "node":
|
||||
clustersResp, err := this.RPC().NodeClusterRPC().ListEnabledNodeClusters(this.AdminContext(), &pb.ListEnabledNodeClustersRequest{
|
||||
Offset: 0,
|
||||
Size: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, cluster := range clustersResp.NodeClusters {
|
||||
_ = this.upgradeCluster("node", cluster.Id)
|
||||
}
|
||||
case "dns":
|
||||
dnsClusters := loadDNSUpgradeModules(&this.ParentAction)
|
||||
for _, c := range dnsClusters {
|
||||
this.upgradeDNSClusterFromMap(c)
|
||||
}
|
||||
case "httpdns":
|
||||
clustersResp, err := this.RPC().HTTPDNSClusterRPC().ListHTTPDNSClusters(this.AdminContext(), &pb.ListHTTPDNSClustersRequest{
|
||||
Offset: 0,
|
||||
Size: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, cluster := range clustersResp.Clusters {
|
||||
_ = this.upgradeCluster("httpdns", cluster.Id)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// upgradeDNSCluster 根据集群ID升级DNS节点
|
||||
func (this *UpgradeNodeAction) upgradeDNSCluster(clusterId int64) {
|
||||
dnsClusters := loadDNSUpgradeModules(&this.ParentAction)
|
||||
for _, c := range dnsClusters {
|
||||
if c.GetInt64("id") == clusterId {
|
||||
this.upgradeDNSClusterFromMap(c)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// upgradeDNSClusterFromMap 从maps.Map中提取节点ID并升级
|
||||
func (this *UpgradeNodeAction) upgradeDNSClusterFromMap(c maps.Map) {
|
||||
nodesVal := c.Get("nodes")
|
||||
if nodeMaps, ok := nodesVal.([]maps.Map); ok {
|
||||
for _, n := range nodeMaps {
|
||||
nodeId := n.GetInt64("id")
|
||||
if nodeId > 0 {
|
||||
_ = upgradeDNSNode(&this.ParentAction, nodeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,20 +25,16 @@ func (this *CreatePopupAction) Init() {
|
||||
func (this *CreatePopupAction) RunGet(params struct{}) {
|
||||
// 检查是否启用了 HTTPDNS 功能(全局用户注册设置中)
|
||||
var hasHTTPDNSFeature = false
|
||||
var defaultHttpdnsClusterId int64 = 0
|
||||
|
||||
resp, err := this.RPC().SysSettingRPC().ReadSysSetting(this.AdminContext(), &pb.ReadSysSettingRequest{Code: systemconfigs.SettingCodeUserRegisterConfig})
|
||||
if err == nil && len(resp.ValueJSON) > 0 {
|
||||
var config = userconfigs.DefaultUserRegisterConfig()
|
||||
if json.Unmarshal(resp.ValueJSON, config) == nil {
|
||||
hasHTTPDNSFeature = config.HTTPDNSIsOn
|
||||
if len(config.HTTPDNSDefaultClusterIds) > 0 {
|
||||
defaultHttpdnsClusterId = config.HTTPDNSDefaultClusterIds[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
this.Data["hasHTTPDNSFeature"] = hasHTTPDNSFeature
|
||||
this.Data["httpdnsClusterId"] = defaultHttpdnsClusterId
|
||||
this.Data["httpdnsClusterId"] = 0
|
||||
|
||||
// 加载所有 HTTPDNS 集群
|
||||
var httpdnsClusters = []maps.Map{}
|
||||
|
||||
@@ -22046,15 +22046,16 @@ Vue.component("ssl-certs-box", {
|
||||
<span class="grey" v-if="description.length > 0">{{description}}</span>
|
||||
<div class="ui divider" v-if="buttonsVisible()"></div>
|
||||
</div>
|
||||
<div v-if="buttonsVisible()">
|
||||
<div v-if="buttonsVisible()" style="display:flex;align-items:center;gap:0.5rem;flex-wrap:nowrap;white-space:nowrap;">
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<span class="disabled" style="margin:0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
|
||||
|
||||
Vue.component("ssl-certs-view", {
|
||||
props: ["v-certs"],
|
||||
data: function () {
|
||||
@@ -22518,12 +22519,16 @@ Vue.component("ssl-config-box", {
|
||||
<span class="red">选择或上传证书后<span v-if="vProtocol == 'https'">HTTPS</span><span v-if="vProtocol == 'tls'">TLS</span>服务才能生效。</span>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="requestCert()" v-if="vServerId != null && vServerId > 0">申请免费证书</button>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: nowrap; white-space: nowrap;">
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled" style="margin: 0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
<template v-if="vServerId != null && vServerId > 0">
|
||||
<span class="disabled" style="margin: 0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="requestCert()">申请免费证书</button>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -22046,15 +22046,16 @@ Vue.component("ssl-certs-box", {
|
||||
<span class="grey" v-if="description.length > 0">{{description}}</span>
|
||||
<div class="ui divider" v-if="buttonsVisible()"></div>
|
||||
</div>
|
||||
<div v-if="buttonsVisible()">
|
||||
<div v-if="buttonsVisible()" style="display:flex;align-items:center;gap:0.5rem;flex-wrap:nowrap;white-space:nowrap;">
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<span class="disabled" style="margin:0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
|
||||
|
||||
Vue.component("ssl-certs-view", {
|
||||
props: ["v-certs"],
|
||||
data: function () {
|
||||
@@ -22518,12 +22519,16 @@ Vue.component("ssl-config-box", {
|
||||
<span class="red">选择或上传证书后<span v-if="vProtocol == 'https'">HTTPS</span><span v-if="vProtocol == 'tls'">TLS</span>服务才能生效。</span>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="requestCert()" v-if="vServerId != null && vServerId > 0">申请免费证书</button>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: nowrap; white-space: nowrap;">
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled" style="margin: 0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
<template v-if="vServerId != null && vServerId > 0">
|
||||
<span class="disabled" style="margin: 0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="requestCert()">申请免费证书</button>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -160,11 +160,11 @@ Vue.component("ssl-certs-box", {
|
||||
<span class="grey" v-if="description.length > 0">{{description}}</span>
|
||||
<div class="ui divider" v-if="buttonsVisible()"></div>
|
||||
</div>
|
||||
<div v-if="buttonsVisible()">
|
||||
<div v-if="buttonsVisible()" style="display:flex;align-items:center;gap:0.5rem;flex-wrap:nowrap;white-space:nowrap;">
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<span class="disabled" style="margin:0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
})
|
||||
|
||||
@@ -427,12 +427,16 @@ Vue.component("ssl-config-box", {
|
||||
<span class="red">选择或上传证书后<span v-if="vProtocol == 'https'">HTTPS</span><span v-if="vProtocol == 'tls'">TLS</span>服务才能生效。</span>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
<span class="disabled">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="requestCert()" v-if="vServerId != null && vServerId > 0">申请免费证书</button>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: nowrap; white-space: nowrap;">
|
||||
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button>
|
||||
<span class="disabled" style="margin: 0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
|
||||
<button class="ui button tiny" type="button" @click.prevent="uploadBatch()">批量上传证书</button>
|
||||
<template v-if="vServerId != null && vServerId > 0">
|
||||
<span class="disabled" style="margin: 0 0.5rem;">|</span>
|
||||
<button class="ui button tiny" type="button" @click.prevent="requestCert()">申请免费证书</button>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<title>{$ htmlEncode .teaTitle}</title>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
||||
{$if eq .teaFaviconFileId 0}
|
||||
<link rel="shortcut icon" href="/images/favicon.png"/>
|
||||
<link rel="shortcut icon" href="/images/favicon.png" />
|
||||
{$else}
|
||||
<link rel="shortcut icon" href="/ui/image/{$.teaFaviconFileId}"/>
|
||||
<link rel="shortcut icon" href="/ui/image/{$.teaFaviconFileId}" />
|
||||
{$end}
|
||||
<link rel="stylesheet" type="text/css" href="/_/@default/@layout.css" media="all"/>
|
||||
<link rel="stylesheet" type="text/css" href="/_/@default/@layout.css" media="all" />
|
||||
{$TEA.SEMANTIC}
|
||||
|
||||
{$TEA.VUE}
|
||||
@@ -20,7 +21,7 @@
|
||||
window.BRAND_DOCS_SITE = {$ jsonEncode .brandConfig.docsSite};
|
||||
window.BRAND_DOCS_PREFIX = {$ jsonEncode .brandConfig.docsPathPrefix};
|
||||
window.BRAND_PRODUCT_NAME = {$ jsonEncode .brandConfig.productName};
|
||||
|
||||
|
||||
// 确保 teaName 和 teaVersion 在 Vue 初始化前可用
|
||||
if (typeof window.TEA === "undefined") {
|
||||
window.TEA = {};
|
||||
@@ -36,122 +37,152 @@
|
||||
</script>
|
||||
<script type="text/javascript" src="/js/config/brand.js"></script>
|
||||
<script type="text/javascript" src="/_/@default/@layout.js"></script>
|
||||
<script type="text/javascript" src="/js/components.js"></script>
|
||||
<script type="text/javascript" src="/js/utils.min.js"></script>
|
||||
<script type="text/javascript" src="/js/sweetalert2/dist/sweetalert2.all.min.js" async></script>
|
||||
<script type="text/javascript" src="/js/date.tea.js"></script>
|
||||
<script type="text/javascript" src="/js/components.js"></script>
|
||||
<script type="text/javascript" src="/js/utils.min.js"></script>
|
||||
<script type="text/javascript" src="/js/sweetalert2/dist/sweetalert2.all.min.js" async></script>
|
||||
<script type="text/javascript" src="/js/date.tea.js"></script>
|
||||
<script type="text/javascript" src="/js/langs/base.js?v={$ .teaVersion}"></script>
|
||||
<script type="text/javascript" src="/js/langs/{$.teaLang}.js?v={$ .teaVersion}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/js/langs/{$.teaLang}.css?v={$ .teaVersion}"/>
|
||||
<link rel="stylesheet" type="text/css" href="/js/langs/{$.teaLang}.css?v={$ .teaVersion}" />
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/_/@default/@layout_override.css" media="all"/>
|
||||
<link rel="stylesheet" type="text/css" href="/_/@default/@layout_override.css" media="all" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div>
|
||||
<!-- 顶部导航 -->
|
||||
<div class="ui menu top-nav blue inverted small borderless" :class="(teaTheme == null || teaTheme.length == 0) ? 'theme2': teaTheme" v-cloak="">
|
||||
<a href="/" class="item">
|
||||
<i class="ui icon leaf" v-if="teaLogoFileId == 0"></i><img alt="logo" v-if="teaLogoFileId > 0" :src="'/ui/image/' + teaLogoFileId" style="width: auto;height: 1.6em"/> {{teaTitle}} <sup v-if="teaShowVersion">v{{teaVersion}}<span v-if="teaVersion.split('.').length == 4" title="当前版本为测试版,再次感谢您参与测试"> beta</span></sup>
|
||||
</a>
|
||||
|
||||
<div class="right menu">
|
||||
<!-- 集群同步 -->
|
||||
<a href="" class="item" v-if="teaCheckNodeTasks && doingNodeTasks.isUpdated" @click.prevent="showNodeTasks()">
|
||||
<span v-if="!doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span"><i class="icon cloud disabled"></i><span class="disabled">已同步节点</span></span>
|
||||
<span v-if="doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span rotate"><i class="icon cloud"></i><span>正在同步节点...</span></span>
|
||||
<span v-if="doingNodeTasks.hasError" class="red"><i class="icon cloud"></i>节点同步失败</span>
|
||||
<div>
|
||||
<!-- 顶部导航 -->
|
||||
<div class="ui menu top-nav blue inverted small borderless"
|
||||
:class="(teaTheme == null || teaTheme.length == 0) ? 'theme2': teaTheme" v-cloak="">
|
||||
<a href="/" class="item">
|
||||
<i class="ui icon leaf" v-if="teaLogoFileId == 0"></i><img alt="logo" v-if="teaLogoFileId > 0"
|
||||
:src="'/ui/image/' + teaLogoFileId" style="width: auto;height: 1.6em" />
|
||||
{{teaTitle}} <sup v-if="teaShowVersion">v{{teaVersion}}<span
|
||||
v-if="teaVersion.split('.').length == 4" title="当前版本为测试版,再次感谢您参与测试"> beta</span></sup>
|
||||
</a>
|
||||
|
||||
<!-- DNS同步 -->
|
||||
<a href="" class="item" v-if="teaCheckDNSTasks && doingDNSTasks.isUpdated" @click.prevent="showDNSTasks()">
|
||||
<span v-if="!doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span"><i class="icon globe disabled"></i><span class="disabled">已同步DNS</span></span>
|
||||
<span v-if="doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span rotate"><i class="icon globe"></i><span>正在同步DNS...</span></span>
|
||||
<span v-if="doingDNSTasks.hasError" class="red"><i class="icon globe"></i>DNS同步失败</span>
|
||||
</a>
|
||||
<div class="right menu">
|
||||
<!-- 集群同步 -->
|
||||
<a href="" class="item" v-if="teaCheckNodeTasks && doingNodeTasks.isUpdated"
|
||||
@click.prevent="showNodeTasks()">
|
||||
<span v-if="!doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span"><i
|
||||
class="icon cloud disabled"></i><span class="disabled">已同步节点</span></span>
|
||||
<span v-if="doingNodeTasks.isDoing && !doingNodeTasks.hasError" class="hover-span rotate"><i
|
||||
class="icon cloud"></i><span>正在同步节点...</span></span>
|
||||
<span v-if="doingNodeTasks.hasError" class="red"><i class="icon cloud"></i>节点同步失败</span>
|
||||
</a>
|
||||
|
||||
<!-- 消息 -->
|
||||
<a href="" class="item" :class="{active:teaMenu == 'message'}" @click.prevent="showMessages()">
|
||||
<span v-if="globalMessageBadge > 0" class="blink hover-span"><i class="icon bell"></i><span>消息({{globalMessageBadge}}) </span></span>
|
||||
<span v-if="globalMessageBadge == 0" class="hover-span"><i class="icon bell disabled"></i><span class="disabled">消息(0)</span></span>
|
||||
</a>
|
||||
<!-- DNS同步 -->
|
||||
<a href="" class="item" v-if="teaCheckDNSTasks && doingDNSTasks.isUpdated"
|
||||
@click.prevent="showDNSTasks()">
|
||||
<span v-if="!doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span"><i
|
||||
class="icon globe disabled"></i><span class="disabled">已同步DNS</span></span>
|
||||
<span v-if="doingDNSTasks.isDoing && !doingDNSTasks.hasError" class="hover-span rotate"><i
|
||||
class="icon globe"></i><span>正在同步DNS...</span></span>
|
||||
<span v-if="doingDNSTasks.hasError" class="red"><i class="icon globe"></i>DNS同步失败</span>
|
||||
</a>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<a href="/settings/profile" class="item">
|
||||
<i class="icon user" v-if="teaUserAvatar.length == 0"></i>
|
||||
<img class="avatar" alt="" :src="teaUserAvatar" v-if="teaUserAvatar.length > 0"/>
|
||||
<span class="hover-span"><span class="disabled">{{teaUsername}}</span></span>
|
||||
</a>
|
||||
<!-- 消息 -->
|
||||
<a href="" class="item" :class="{active:teaMenu == 'message'}" @click.prevent="showMessages()">
|
||||
<span v-if="globalMessageBadge > 0" class="blink hover-span"><i
|
||||
class="icon bell"></i><span>消息({{globalMessageBadge}}) </span></span>
|
||||
<span v-if="globalMessageBadge == 0" class="hover-span"><i class="icon bell disabled"></i><span
|
||||
class="disabled">消息(0)</span></span>
|
||||
</a>
|
||||
|
||||
<a href="" class="item" title="switch language" @click.prevent="switchLang" v-show="false"><i class="icon language"></i> </a>
|
||||
<!-- 用户信息 -->
|
||||
<a href="/settings/profile" class="item">
|
||||
<i class="icon user" v-if="teaUserAvatar.length == 0"></i>
|
||||
<img class="avatar" alt="" :src="teaUserAvatar" v-if="teaUserAvatar.length > 0" />
|
||||
<span class="hover-span"><span class="disabled">{{teaUsername}}</span></span>
|
||||
</a>
|
||||
|
||||
<!-- 背景颜色 -->
|
||||
<a href="" class="item" title="点击切换界面风格" @click.prevent="changeTheme()" v-if="false"><i class="icon adjust"></i></a>
|
||||
<a href="" class="item" title="switch language" @click.prevent="switchLang" v-show="false"><i
|
||||
class="icon language"></i> </a>
|
||||
|
||||
<!-- 企业版 -->
|
||||
<!-- <a :href="'/settings/authority'" class="item" title="商业版" :v-if="teaIsPlus"><i class="icon gem outline yellow"></i></a>-->
|
||||
<!-- 背景颜色 -->
|
||||
<a href="" class="item" title="点击切换界面风格" @click.prevent="changeTheme()" v-if="false"><i
|
||||
class="icon adjust"></i></a>
|
||||
|
||||
<!-- 退出登录 -->
|
||||
<a :href="Tea.url('logout')" class="item" title="安全退出登录"><i class="icon sign out"></i>
|
||||
<span class="hover-span"><span class="disabled">退出登录</span></span>
|
||||
</a>
|
||||
<!-- 企业版 -->
|
||||
<!-- <a :href="'/settings/authority'" class="item" title="商业版" :v-if="teaIsPlus"><i class="icon gem outline yellow"></i></a>-->
|
||||
|
||||
<!-- 退出登录 -->
|
||||
<a :href="Tea.url('logout')" class="item" title="安全退出登录"><i class="icon sign out"></i>
|
||||
<span class="hover-span"><span class="disabled">退出登录</span></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 左侧主菜单 -->
|
||||
<div class="main-menu" :class="(teaTheme == null || teaTheme.length == 0) ? 'theme2': teaTheme" v-cloak="">
|
||||
<div class="ui labeled menu vertical blue inverted tiny borderless">
|
||||
<div class="item"></div>
|
||||
<!--<a :href="Tea.url('dashboard')" class="item" :class="{active:teaMenu == 'dashboard'}">
|
||||
<!-- 左侧主菜单 -->
|
||||
<div class="main-menu" :class="(teaTheme == null || teaTheme.length == 0) ? 'theme2': teaTheme" v-cloak="">
|
||||
<div class="ui labeled menu vertical blue inverted tiny borderless">
|
||||
<div class="item"></div>
|
||||
<!--<a :href="Tea.url('dashboard')" class="item" :class="{active:teaMenu == 'dashboard'}">
|
||||
<i class="ui dashboard icon"></i>
|
||||
<span>仪表板</span>
|
||||
</a>-->
|
||||
|
||||
<!-- 模块 -->
|
||||
<div v-for="module in teaModules">
|
||||
<a class="item" :href="Tea.url(module.code)" :class="{active:teaMenu == module.code && teaSubMenu.length == 0, separator:module.code.length == 0, expend: teaMenu == module.code}" :style="(teaMenu == module.code && teaSubMenu.length == 0) ? 'background: rgba(230, 230, 230, 0.45) !important;' : ''" v-if="module.isOn !== false">
|
||||
<span v-if="module.code.length > 0">
|
||||
<i class="window restore outline icon" v-if="module.icon == null"></i>
|
||||
<i class="ui icon" v-if="module.icon != null" :class="module.icon"></i>
|
||||
<span class="module-name">{{module.name}}</span>
|
||||
</span>
|
||||
<div class="subtitle" v-if="module.subtitle != null && module.subtitle.length > 0">{{module.subtitle}}</div>
|
||||
</a>
|
||||
<div v-if="teaMenu == module.code" class="sub-items">
|
||||
<a class="item" :class="{separator:subItem.name == '-', active: subItem.code == teaSubMenu}" v-for="subItem in module.subItems" v-if="subItem.isOn !== false" :href="subItem.url" :style="(subItem.code == teaSubMenu) ? 'background: rgba(230, 230, 230, 0.55) !important;' : ''"><i class="icon angle right" v-if="subItem.name != '-' && subItem.code == teaSubMenu"></i> <span v-if="subItem.name != '-'">{{subItem.name}}</span>
|
||||
<span class="ui label tiny red" v-if="subItem.badge != null && subItem.badge > 0">
|
||||
<span v-if="subItem.badge < 100">{{subItem.badge}}</span>
|
||||
<span v-else>99+</span>
|
||||
<!-- 模块 -->
|
||||
<div v-for="module in teaModules">
|
||||
<a class="item" :href="Tea.url(module.code)"
|
||||
:class="{active:teaMenu == module.code && teaSubMenu.length == 0, separator:module.code.length == 0, expend: teaMenu == module.code}"
|
||||
:style="(teaMenu == module.code && teaSubMenu.length == 0) ? 'background: rgba(230, 230, 230, 0.45) !important;' : ''"
|
||||
v-if="module.isOn !== false">
|
||||
<span v-if="module.code.length > 0">
|
||||
<i class="window restore outline icon" v-if="module.icon == null"></i>
|
||||
<i class="ui icon" v-if="module.icon != null" :class="module.icon"></i>
|
||||
<span class="module-name">{{module.name}}</span>
|
||||
</span>
|
||||
<div class="subtitle" v-if="module.subtitle != null && module.subtitle.length > 0">
|
||||
{{module.subtitle}}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="teaMenu == module.code" class="sub-items">
|
||||
<a class="item" :class="{separator:subItem.name == '-', active: subItem.code == teaSubMenu}"
|
||||
v-for="subItem in module.subItems" v-if="subItem.isOn !== false" :href="subItem.url"
|
||||
:style="(subItem.code == teaSubMenu) ? 'background: rgba(230, 230, 230, 0.55) !important;' : ''"><i
|
||||
class="icon angle right" v-if="subItem.name != '-' && subItem.code == teaSubMenu"></i>
|
||||
<span v-if="subItem.name != '-'">{{subItem.name}}</span>
|
||||
<span class="ui label tiny red" v-if="subItem.badge != null && subItem.badge > 0">
|
||||
<span v-if="subItem.badge < 100">{{subItem.badge}}</span>
|
||||
<span v-else>99+</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧主操作栏 -->
|
||||
<div class="main"
|
||||
:class="{'without-menu':teaSubMenus.menus == null || teaSubMenus.menus.length == 0 || (teaSubMenus.menus.length == 1 && teaSubMenus.menus[0].alwaysActive), 'without-secondary-menu':teaSubMenus.alwaysMenu == null || teaSubMenus.alwaysMenu.items.length <= 1, 'without-footer':!teaShowOpenSourceInfo}"
|
||||
v-cloak="">
|
||||
<!-- 操作菜单 -->
|
||||
<div class="ui top menu tabular tab-menu small" v-if="teaTabbar.length > 1">
|
||||
<a class="item" v-for="item in teaTabbar"
|
||||
:class="{'active':item.isActive && !item.isDisabled, right:item.isRight, title: item.isTitle, icon: item.icon != null && item.icon.length > 0, disabled: item.isDisabled}"
|
||||
:href="item.url">
|
||||
<var>{{item.name}}<span v-if="item.subName.length > 0">({{item.subName}})</span><i
|
||||
class="icon small" :class="item.icon" v-if="item.icon != null && item.icon.length > 0"></i>
|
||||
</var>
|
||||
<var v-if="item.isTitle && typeof _data.node == 'object'">{{node.name}}</var>
|
||||
<div class="bottom-indicator" v-if="item.isActive && !item.isTitle"></div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 功能区 -->
|
||||
<div class="main-box">
|
||||
{$TEA.VIEW}
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部 -->
|
||||
{$template "/footer"}
|
||||
</div>
|
||||
|
||||
<!-- 右侧主操作栏 -->
|
||||
<div class="main" :class="{'without-menu':teaSubMenus.menus == null || teaSubMenus.menus.length == 0 || (teaSubMenus.menus.length == 1 && teaSubMenus.menus[0].alwaysActive), 'without-secondary-menu':teaSubMenus.alwaysMenu == null || teaSubMenus.alwaysMenu.items.length <= 1, 'without-footer':!teaShowOpenSourceInfo}" v-cloak="">
|
||||
<!-- 操作菜单 -->
|
||||
<div class="ui top menu tabular tab-menu small" v-if="teaTabbar.length > 1">
|
||||
<a class="item" v-for="item in teaTabbar" :class="{'active':item.isActive && !item.isDisabled, right:item.isRight, title: item.isTitle, icon: item.icon != null && item.icon.length > 0, disabled: item.isDisabled}" :href="item.url">
|
||||
<var>{{item.name}}<span v-if="item.subName.length > 0">({{item.subName}})</span><i class="icon small" :class="item.icon" v-if="item.icon != null && item.icon.length > 0"></i> </var>
|
||||
<var v-if="item.isTitle && typeof _data.node == 'object'">{{node.name}}</var>
|
||||
<div class="bottom-indicator" v-if="item.isActive && !item.isTitle"></div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 功能区 -->
|
||||
<div class="main-box">
|
||||
{$TEA.VIEW}
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部 -->
|
||||
{$template "/footer"}
|
||||
</div>
|
||||
|
||||
{$echo "footer"}
|
||||
{$echo "footer"}
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -3,60 +3,76 @@
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
a.disabled,
|
||||
a.disabled:hover,
|
||||
a.disabled:active,
|
||||
span.disabled {
|
||||
color: #ccc !important;
|
||||
}
|
||||
|
||||
a.enabled,
|
||||
span.enabled,
|
||||
span.green {
|
||||
color: #21ba45;
|
||||
}
|
||||
|
||||
span.grey,
|
||||
label.grey,
|
||||
p.grey {
|
||||
color: grey !important;
|
||||
}
|
||||
|
||||
p.grey {
|
||||
margin-top: 0.8em;
|
||||
}
|
||||
|
||||
span.red,
|
||||
pre.red {
|
||||
color: #db2828;
|
||||
}
|
||||
|
||||
span.blue {
|
||||
color: #4183c4;
|
||||
}
|
||||
|
||||
pre:not(.CodeMirror-line) {
|
||||
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif !important;
|
||||
}
|
||||
|
||||
tbody {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.table.width30 {
|
||||
width: 30em !important;
|
||||
}
|
||||
|
||||
.table.width35 {
|
||||
width: 35em !important;
|
||||
}
|
||||
|
||||
.table.width40 {
|
||||
width: 40em !important;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
font-size: 0.9em !important;
|
||||
}
|
||||
|
||||
p.comment,
|
||||
div.comment {
|
||||
color: #959da6;
|
||||
@@ -65,37 +81,46 @@ div.comment {
|
||||
word-break: break-all;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
p.comment em,
|
||||
div.comment em {
|
||||
font-style: italic !important;
|
||||
}
|
||||
|
||||
.truncate {
|
||||
white-space: nowrap;
|
||||
-ms-text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
div.margin,
|
||||
p.margin {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
/** 操作按钮容器 **/
|
||||
.op.one {
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
.op.two {
|
||||
width: 7.4em;
|
||||
}
|
||||
|
||||
.op.three {
|
||||
width: 9em;
|
||||
}
|
||||
|
||||
.op.four {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
/** 扩展UI **/
|
||||
.field.text {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
/** 右侧主操作区 **/
|
||||
.main {
|
||||
position: absolute;
|
||||
@@ -105,166 +130,210 @@ p.margin {
|
||||
padding-right: 1em;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 512px) {
|
||||
.main {
|
||||
left: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.main.without-menu {
|
||||
left: 9em;
|
||||
}
|
||||
|
||||
.main.without-secondary-menu {
|
||||
top: 2.9em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 512px) {
|
||||
.main.without-menu {
|
||||
left: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.main table td.title {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.main table td.middle-title {
|
||||
width: 14em;
|
||||
}
|
||||
|
||||
.main table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table td.color-border {
|
||||
border-left: 1px #276ac6 solid !important;
|
||||
}
|
||||
|
||||
.main table td.vertical-top {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.main table td.vertical-middle {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.main table td[colspan="2"] a {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.main table td em {
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.main h3 {
|
||||
font-weight: normal;
|
||||
margin-top: 0.5em !important;
|
||||
}
|
||||
|
||||
.main h3 span {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.main h3 span.label {
|
||||
color: #6435c9;
|
||||
}
|
||||
|
||||
.main h3 a {
|
||||
margin-left: 1em;
|
||||
font-size: 14px !important;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
.main h3 a::before {
|
||||
content: "[";
|
||||
}
|
||||
|
||||
.main h3 a::after {
|
||||
content: "]";
|
||||
}
|
||||
|
||||
.main h4 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.main td span.small {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.main .button.mini {
|
||||
font-size: 0.8em;
|
||||
padding: 0.2em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
/** 右侧文本子菜单 **/
|
||||
.text.menu {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.text.menu::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
/** Vue **/
|
||||
[v-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/** auto complete **/
|
||||
.autocomplete-box .menu {
|
||||
background: #eee !important;
|
||||
}
|
||||
|
||||
.autocomplete-box .menu::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.autocomplete-box .menu .item {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
select.auto-width {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
/** column **/
|
||||
@media screen and (max-width: 512px) {
|
||||
.column:not(.one) {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/** label **/
|
||||
label[for] {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
label.blue {
|
||||
color: #2185d0 !important;
|
||||
}
|
||||
|
||||
/** Menu **/
|
||||
.first-menu .menu.text {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.first-menu .divider {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.second-menu .menu.text {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.second-menu .menu.text em {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.second-menu .divider {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.menu a {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/** var **/
|
||||
span.olive,
|
||||
var.olive {
|
||||
color: #b5cc18 !important;
|
||||
}
|
||||
|
||||
span.dash {
|
||||
border-bottom: 1px dashed grey;
|
||||
}
|
||||
|
||||
span.hover:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
/** Message **/
|
||||
.message .gopher {
|
||||
width: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/** checkbox **/
|
||||
.checkbox label a,
|
||||
.checkbox label {
|
||||
font-size: 0.8em !important;
|
||||
}
|
||||
|
||||
/** page **/
|
||||
.page {
|
||||
margin-top: 1em;
|
||||
border-left: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.page a {
|
||||
display: inline-block;
|
||||
background: #fafafa;
|
||||
@@ -275,40 +344,50 @@ span.hover:hover {
|
||||
border: 1px solid #ddd;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.page a.active {
|
||||
background: #2185d0 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page a:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.page select {
|
||||
padding-top: 0.3em !important;
|
||||
padding-bottom: 0.3em !important;
|
||||
}
|
||||
|
||||
.swal2-html-container {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.swal2-confirm:focus,
|
||||
.swal2-cancel:focus,
|
||||
.swal2-close:focus {
|
||||
border: 3px #ddd solid !important;
|
||||
}
|
||||
|
||||
.swal2-confirm,
|
||||
.swal2-cancel,
|
||||
.swal2-close {
|
||||
border: 3px #fff solid !important;
|
||||
}
|
||||
|
||||
.swal2-cancel {
|
||||
margin-left: 2em !important;
|
||||
}
|
||||
|
||||
input.error {
|
||||
border: 1px #e0b4b4 solid !important;
|
||||
}
|
||||
|
||||
textarea.wide-code {
|
||||
font-family: Menlo, Monaco, "Courier New", monospace !important;
|
||||
line-height: 1.6 !important;
|
||||
}
|
||||
|
||||
.combo-box .menu {
|
||||
max-height: 17em;
|
||||
overflow-y: auto;
|
||||
@@ -317,9 +396,11 @@ textarea.wide-code {
|
||||
border-top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.combo-box .menu::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
code-label {
|
||||
background: #fff;
|
||||
border: 1px solid rgba(34, 36, 38, 0.15);
|
||||
@@ -333,4 +414,62 @@ code-label {
|
||||
font-weight: 700;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/*# sourceMappingURL=@layout_popup.css.map */
|
||||
|
||||
/*# sourceMappingURL=@layout_popup.css.map */
|
||||
/* Override Primary Button Color for WAF Platform */
|
||||
.ui.primary.button,
|
||||
.ui.primary.buttons .button {
|
||||
background-color: #0f2c54 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.ui.primary.button:hover,
|
||||
.ui.primary.buttons .button:hover {
|
||||
background-color: #0a1f3a !important;
|
||||
}
|
||||
|
||||
.ui.primary.button:focus,
|
||||
.ui.primary.buttons .button:focus {
|
||||
background-color: #08192e !important;
|
||||
}
|
||||
|
||||
.ui.primary.button:active,
|
||||
.ui.primary.buttons .button:active,
|
||||
.ui.primary.active.button {
|
||||
background-color: #050d18 !important;
|
||||
}
|
||||
|
||||
.text-primary,
|
||||
.blue {
|
||||
color: #0f2c54 !important;
|
||||
}
|
||||
|
||||
/* Override Semantic UI Default Blue */
|
||||
.ui.blue.button,
|
||||
.ui.blue.buttons .button {
|
||||
background-color: #0f2c54 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.ui.blue.button:hover,
|
||||
.ui.blue.buttons .button:hover {
|
||||
background-color: #0a1f3a !important;
|
||||
}
|
||||
|
||||
.ui.basic.blue.button,
|
||||
.ui.basic.blue.buttons .button {
|
||||
box-shadow: 0 0 0 1px #0f2c54 inset !important;
|
||||
color: #0f2c54 !important;
|
||||
}
|
||||
|
||||
.ui.basic.blue.button:hover,
|
||||
.ui.basic.blue.buttons .button:hover {
|
||||
background: transparent !important;
|
||||
box-shadow: 0 0 0 1px #0a1f3a inset !important;
|
||||
color: #0a1f3a !important;
|
||||
}
|
||||
|
||||
.ui.menu .active.item {
|
||||
border-color: #2185d0 !important;
|
||||
color: #2185d0 !important;
|
||||
}
|
||||
@@ -8,16 +8,10 @@
|
||||
<div class="item"><strong>上传 SDK</strong></div>
|
||||
</second-menu>
|
||||
|
||||
<form method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="ui form"
|
||||
data-tea-action="$"
|
||||
data-tea-timeout="300"
|
||||
data-tea-before="beforeUpload"
|
||||
data-tea-done="doneUpload"
|
||||
data-tea-success="successUpload">
|
||||
<form method="post" enctype="multipart/form-data" class="ui form" data-tea-action="$" data-tea-timeout="300"
|
||||
data-tea-before="beforeUpload" data-tea-done="doneUpload" data-tea-success="successUpload">
|
||||
<csrf-token></csrf-token>
|
||||
<input type="hidden" name="appId" :value="app.id"/>
|
||||
<input type="hidden" name="appId" :value="app.id" />
|
||||
|
||||
<table class="ui table selectable definition">
|
||||
<tr>
|
||||
@@ -33,22 +27,22 @@
|
||||
<tr>
|
||||
<td class="title">版本号 *</td>
|
||||
<td>
|
||||
<input type="text" name="version" v-model="version" maxlength="32"/>
|
||||
<input type="text" name="version" v-model="version" maxlength="32" />
|
||||
<p class="comment">默认 `1.0.0`。同平台+同版本再次上传会覆盖该版本文件。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">SDK 包</td>
|
||||
<td>
|
||||
<input type="file" name="sdkFile" accept=".zip"/>
|
||||
<p class="comment">支持 zip 包,例如 `httpdns-sdk-android.zip`。</p>
|
||||
<input type="file" name="sdkFile" accept=".zip" />
|
||||
<p class="comment">支持 zip 包,例如 `httpdns-sdk-android.zip`,单文件最大 20MB。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">集成文档</td>
|
||||
<td>
|
||||
<input type="file" name="docFile" accept=".md,text/markdown"/>
|
||||
<p class="comment">支持 Markdown 文件(`.md`)。</p>
|
||||
<input type="file" name="docFile" accept=".md,text/markdown" />
|
||||
<p class="comment">支持 Markdown 文件(`.md`),单文件最大 20MB。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -65,26 +59,26 @@
|
||||
<h4 style="margin-top: 1.5em">已上传文件</h4>
|
||||
<table class="ui table selectable celled" v-if="uploadedFiles.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>平台</th>
|
||||
<th>类型</th>
|
||||
<th>版本</th>
|
||||
<th>文件名</th>
|
||||
<th>大小</th>
|
||||
<th>更新时间</th>
|
||||
<th class="one wide">操作</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>平台</th>
|
||||
<th>类型</th>
|
||||
<th>版本</th>
|
||||
<th>文件名</th>
|
||||
<th>大小</th>
|
||||
<th>更新时间</th>
|
||||
<th class="one wide">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="file in uploadedFiles">
|
||||
<td>{{file.platform}}</td>
|
||||
<td>{{file.fileType}}</td>
|
||||
<td>{{file.version}}</td>
|
||||
<td>{{file.name}}</td>
|
||||
<td>{{file.sizeText}}</td>
|
||||
<td>{{file.updatedAt}}</td>
|
||||
<td><a href="" @click.prevent="deleteUploadedFile(file.name)">删除</a></td>
|
||||
</tr>
|
||||
<tr v-for="file in uploadedFiles">
|
||||
<td>{{file.platform}}</td>
|
||||
<td>{{file.fileType}}</td>
|
||||
<td>{{file.version}}</td>
|
||||
<td>{{file.name}}</td>
|
||||
<td>{{file.sizeText}}</td>
|
||||
<td>{{file.updatedAt}}</td>
|
||||
<td><a href="" @click.prevent="deleteUploadedFile(file.name)">删除</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="ui message" v-else>
|
||||
|
||||
@@ -8,6 +8,17 @@
|
||||
}
|
||||
|
||||
this.beforeUpload = function () {
|
||||
var maxSize = 20 * 1024 * 1024; // 20MB
|
||||
var inputs = document.querySelectorAll("input[type=file]");
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
var files = inputs[i].files;
|
||||
if (files != null && files.length > 0) {
|
||||
if (files[0].size > maxSize) {
|
||||
teaweb.warn("文件 \"" + files[0].name + "\" 超过 20MB 限制(" + (files[0].size / 1024 / 1024).toFixed(1) + "MB),请压缩后重试");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.isUploading = true;
|
||||
};
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
<td>
|
||||
v{{node.status.buildVersion}}
|
||||
|
||||
<a :href="'/httpdns/clusters/upgradeRemote?clusterId=' + clusterId + '&nodeId=' + node.id" v-if="shouldUpgrade"><span class="red">发现新版本 v{{newVersion}} »</span></a>
|
||||
<a :href="'/httpdns/clusters/cluster/upgradeRemote?clusterId=' + clusterId" v-if="shouldUpgrade"><span class="red">发现新版本 v{{newVersion}} »</span></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="node.status.exePath != null && node.status.exePath.length > 0">
|
||||
|
||||
@@ -66,30 +66,37 @@
|
||||
<tr>
|
||||
<td>自动远程启动</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="autoRemoteStart" value="1" v-model="settings.autoRemoteStart" />
|
||||
<label></label>
|
||||
</div>
|
||||
<checkbox name="autoRemoteStart" v-model="settings.autoRemoteStart"></checkbox>
|
||||
<p class="comment">当检测到节点离线时,自动尝试远程启动(前提是节点已经设置了SSH登录认证)。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>访问日志</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="accessLogIsOn" value="1" v-model="settings.accessLogIsOn" />
|
||||
<label></label>
|
||||
</div>
|
||||
<checkbox name="accessLogIsOn" v-model="settings.accessLogIsOn"></checkbox>
|
||||
<p class="comment">启用后,HTTPDNS 节点将会记录客户端的请求访问日志。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>时区</td>
|
||||
<td>
|
||||
<div>
|
||||
<span class="ui label basic small" v-if="timeZoneLocation != null">当前:{{timeZoneLocation.name}}
|
||||
({{timeZoneLocation.offset}})</span>
|
||||
</div>
|
||||
<div style="margin-top: 0.5em">
|
||||
<select class="ui dropdown auto-width" name="timeZone" v-model="settings.timeZone">
|
||||
<option v-for="tz in timeZoneLocations" :value="tz.name">{{tz.name}} ({{tz.offset}})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="comment">HTTPDNS 节点进程使用的时区设置,默认为 Asia/Shanghai。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>启用当前集群</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="isOn" value="1" v-model="settings.isOn" />
|
||||
<label></label>
|
||||
</div>
|
||||
<checkbox name="isOn" v-model="settings.isOn"></checkbox>
|
||||
<p class="comment">取消启用后,该集群不会参与 HTTPDNS 服务。</p>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -110,4 +117,4 @@
|
||||
|
||||
<submit-btn></submit-btn>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,4 +7,27 @@ Tea.context(function () {
|
||||
if (!this.settings) {
|
||||
this.settings = {}
|
||||
}
|
||||
|
||||
let toBool = function (v) {
|
||||
if (typeof v === "boolean") {
|
||||
return v
|
||||
}
|
||||
if (typeof v === "number") {
|
||||
return v === 1
|
||||
}
|
||||
if (typeof v === "string") {
|
||||
let s = v.toLowerCase().trim()
|
||||
return s === "1" || s === "true" || s === "on" || s === "yes" || s === "enabled"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
this.settings.autoRemoteStart = toBool(this.settings.autoRemoteStart)
|
||||
this.settings.accessLogIsOn = toBool(this.settings.accessLogIsOn)
|
||||
this.settings.isOn = toBool(this.settings.isOn)
|
||||
|
||||
if (!this.settings.timeZone || this.settings.timeZone.length == 0) {
|
||||
this.settings.timeZone = "Asia/Shanghai"
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<tr>
|
||||
<td>节点安装根目录</td>
|
||||
<td>
|
||||
<input type="text" name="installDir" maxlength="255" value="/opt/edge-httpdns" />
|
||||
<input type="text" name="installDir" maxlength="255" value="/root/edge-httpdns" />
|
||||
<p class="comment">边缘节点安装 HTTPDNS 服务的默认目录。</p>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -59,4 +59,4 @@
|
||||
</tr>
|
||||
</table>
|
||||
<submit-btn></submit-btn>
|
||||
</form>
|
||||
</form>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<tr>
|
||||
<td>安装目录</td>
|
||||
<td>
|
||||
<input type="text" name="installDir" maxlength="100" :value="cluster.installDir || '/opt/edge-httpdns'" />
|
||||
<input type="text" name="installDir" maxlength="100" :value="cluster.installDir || '/root/edge-httpdns'" />
|
||||
<p class="comment">默认使用集群配置目录。</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -51,6 +51,12 @@
|
||||
:class="{red:cluster.countAllNodes > cluster.countActiveNodes}">{{cluster.countAllNodes}}</span>
|
||||
</a>
|
||||
<span class="disabled" v-else="">-</span>
|
||||
|
||||
<div v-if="cluster.countUpgradeNodes > 0" style="margin-top:0.5em">
|
||||
<a :href="'/httpdns/clusters/cluster/upgradeRemote?clusterId=' + cluster.id" title="点击进入远程升级页面">
|
||||
<span class="red">有{{cluster.countUpgradeNodes}}个节点需要升级</span>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="center">
|
||||
<a :href="'/httpdns/clusters/cluster?clusterId=' + cluster.id" v-if="cluster.countActiveNodes > 0">
|
||||
@@ -67,4 +73,4 @@
|
||||
</table>
|
||||
|
||||
<div class="page" v-html="page"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,62 @@
|
||||
{$layout}
|
||||
{$layout}
|
||||
|
||||
<p class="ui message error">姝ゅ姛鑳芥殏鏈紑鏀俱€?/p>
|
||||
<p class="comment" v-if="nodes.length == 0">暂时没有需要升级的节点。</p>
|
||||
|
||||
<div v-if="nodes.length > 0">
|
||||
<h3>
|
||||
所有需要升级的节点
|
||||
<button class="ui button primary tiny" v-if="countCheckedNodes() > 0" @click.prevent="installBatch()">批量升级({{countCheckedNodes()}})</button>
|
||||
</h3>
|
||||
<table class="ui table selectable celled">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:3em">
|
||||
<checkbox @input="checkNodes"></checkbox>
|
||||
</th>
|
||||
<th>节点名</th>
|
||||
<th>访问IP</th>
|
||||
<th>SSH地址</th>
|
||||
<th>版本变化</th>
|
||||
<th class="four wide">节点状态</th>
|
||||
<th class="two op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="node in nodes">
|
||||
<td>
|
||||
<checkbox v-model="node.isChecked" v-if="node.installStatus == null || !node.installStatus.isOk"></checkbox>
|
||||
</td>
|
||||
<td>
|
||||
<a :href="'/httpdns/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + node.id">{{node.name}}</a>
|
||||
<a :href="'/httpdns/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + node.id" title="节点详情" style="margin-left: 0.4em">[详情]</a>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="node.accessIP.length > 0" class="ui label tiny basic">{{node.accessIP}}</span>
|
||||
<span v-else class="disabled">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="node.login != null && node.login.type == 'ssh' && node.loginParams != null && node.loginParams.host != null && node.loginParams.host.length > 0">
|
||||
{{node.loginParams.host}}:{{node.loginParams.port}}
|
||||
</span>
|
||||
<span v-else class="disabled">没有设置</span>
|
||||
</td>
|
||||
<td>v{{node.oldVersion}} -> v{{node.newVersion}}</td>
|
||||
<td>
|
||||
<div v-if="node.installStatus != null && (node.installStatus.isRunning || node.installStatus.isFinished)">
|
||||
<div v-if="node.installStatus.isRunning" class="blue">升级中...</div>
|
||||
<div v-if="node.installStatus.isFinished">
|
||||
<span v-if="node.installStatus.isOk" class="green">已升级成功</span>
|
||||
<span v-if="!node.installStatus.isOk" class="red">升级过程中发生错误:{{node.installStatus.error}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="disabled">等待升级</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="" @click.prevent="installNode(node)" v-if="!isInstalling">升级</a>
|
||||
<span v-if="isInstalling && node.isInstalling">升级中...</span>
|
||||
<span v-if="isInstalling && !node.isInstalling" class="disabled">升级</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
147
EdgeAdmin/web/views/@default/httpdns/clusters/upgradeRemote.js
Normal file
147
EdgeAdmin/web/views/@default/httpdns/clusters/upgradeRemote.js
Normal file
@@ -0,0 +1,147 @@
|
||||
Tea.context(function () {
|
||||
this.isInstalling = false
|
||||
this.isBatch = false
|
||||
let installingNode = null
|
||||
|
||||
this.nodes.forEach(function (v) {
|
||||
v.isChecked = false
|
||||
})
|
||||
|
||||
this.$delay(function () {
|
||||
this.reload()
|
||||
})
|
||||
|
||||
let that = this
|
||||
|
||||
this.checkNodes = function (isChecked) {
|
||||
this.nodes.forEach(function (v) {
|
||||
v.isChecked = isChecked
|
||||
})
|
||||
}
|
||||
|
||||
this.countCheckedNodes = function () {
|
||||
return that.nodes.$count(function (k, v) {
|
||||
return v.isChecked
|
||||
})
|
||||
}
|
||||
|
||||
this.installNode = function (node) {
|
||||
let that = this
|
||||
if (this.isBatch) {
|
||||
installingNode = node
|
||||
that.isInstalling = true
|
||||
node.isInstalling = true
|
||||
|
||||
that.$post("$")
|
||||
.params({
|
||||
nodeId: node.id
|
||||
})
|
||||
} else {
|
||||
teaweb.confirm("确定要开始升级此节点吗?", function () {
|
||||
installingNode = node
|
||||
that.isInstalling = true
|
||||
node.isInstalling = true
|
||||
|
||||
that.$post("$")
|
||||
.params({
|
||||
nodeId: node.id
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.installBatch = function () {
|
||||
let that = this
|
||||
this.isBatch = true
|
||||
teaweb.confirm("确定要批量升级选中的节点吗?", function () {
|
||||
that.installNext()
|
||||
})
|
||||
}
|
||||
|
||||
this.installNext = function () {
|
||||
let nextNode = this.nodes.$find(function (k, v) {
|
||||
return v.isChecked
|
||||
})
|
||||
|
||||
if (nextNode == null) {
|
||||
teaweb.success("全部升级成功", function () {
|
||||
teaweb.reload()
|
||||
})
|
||||
} else {
|
||||
this.installNode(nextNode)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.reload = function () {
|
||||
let that = this
|
||||
if (installingNode != null) {
|
||||
this.$post("/httpdns/clusters/upgradeStatus")
|
||||
.params({
|
||||
nodeId: installingNode.id
|
||||
})
|
||||
.success(function (resp) {
|
||||
if (resp.data.status != null) {
|
||||
installingNode.installStatus = resp.data.status
|
||||
if (installingNode.installStatus.isFinished) {
|
||||
if (installingNode.installStatus.isOk) {
|
||||
installingNode.isChecked = false
|
||||
installingNode = null
|
||||
if (that.isBatch) {
|
||||
that.installNext()
|
||||
} else {
|
||||
teaweb.success("升级成功", function () {
|
||||
teaweb.reload()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let nodeId = installingNode.id
|
||||
let errMsg = installingNode.installStatus.error
|
||||
that.isInstalling = false
|
||||
installingNode.isInstalling = false
|
||||
installingNode = null
|
||||
|
||||
switch (resp.data.status.errorCode) {
|
||||
case "EMPTY_LOGIN":
|
||||
case "EMPTY_SSH_HOST":
|
||||
case "EMPTY_SSH_PORT":
|
||||
case "EMPTY_GRANT":
|
||||
teaweb.warn("需要填写SSH登录信息", function () {
|
||||
teaweb.popup("/httpdns/clusters/updateNodeSSH?nodeId=" + nodeId, {
|
||||
height: "20em",
|
||||
callback: function () {
|
||||
teaweb.reload()
|
||||
}
|
||||
})
|
||||
})
|
||||
return
|
||||
case "CREATE_ROOT_DIRECTORY_FAILED":
|
||||
teaweb.warn("创建根目录失败,请检查目录权限或手工创建:" + errMsg)
|
||||
return
|
||||
case "INSTALL_HELPER_FAILED":
|
||||
teaweb.warn("安装助手失败:" + errMsg)
|
||||
return
|
||||
case "TEST_FAILED":
|
||||
teaweb.warn("环境测试失败:" + errMsg)
|
||||
return
|
||||
case "RPC_TEST_FAILED":
|
||||
teaweb.confirm("html:要升级的节点到API服务之间的RPC通讯测试失败,具体错误:" + errMsg + ",<br/>现在修改API信息?", function () {
|
||||
window.location = "/settings/api"
|
||||
})
|
||||
return
|
||||
default:
|
||||
teaweb.warn("升级失败:" + errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.done(function () {
|
||||
setTimeout(this.reload, 3000)
|
||||
})
|
||||
} else {
|
||||
setTimeout(this.reload, 3000)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</thead>
|
||||
<tr v-for="node in nodes">
|
||||
<td class="node-name-td"><a :href="'/ns/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + node.id"><keyword :v-word="keyword">{{node.name}}</keyword></a>
|
||||
<div v-if="node.status != null && node.status.version.length > 0 && node.status.version != latestVersion">
|
||||
<div v-if="node.status != null && node.status.version.length > 0 && latestVersion.length > 0 && Tea.versionCompare(latestVersion, node.status.version) > 0">
|
||||
<span class="small red">v{{node.status.version}} -> v{{latestVersion}}</span>
|
||||
</div>
|
||||
|
||||
@@ -112,4 +112,4 @@
|
||||
</table>
|
||||
|
||||
|
||||
<div class="page" v-html="page"></div>
|
||||
<div class="page" v-html="page"></div>
|
||||
|
||||
@@ -97,4 +97,4 @@
|
||||
</tr>
|
||||
</table>
|
||||
<p class="comment">数据更新于{{key.updatedTime}}。</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,45 +1,38 @@
|
||||
{$layout}
|
||||
|
||||
<form class="ui form" method="post" data-tea-action="$" data-tea-success="success">
|
||||
<csrf-token></csrf-token>
|
||||
<csrf-token></csrf-token>
|
||||
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">产品名称 *</td>
|
||||
<td>
|
||||
<input type="text" name="productName" v-model="config.productName" maxlength="100"/>
|
||||
<p class="comment">可以使用变量<code-label>${product.name}</code-label>在网页里展示此名称,建议仅包含英文和数字字符。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>管理员系统名称 *</td>
|
||||
<td>
|
||||
<input type="text" name="adminSystemName" v-model="config.adminSystemName" maxlength="100"/>
|
||||
<p class="comment">当前管理系统界面上显示的名称。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>显示版本号</td>
|
||||
<td>
|
||||
<checkbox name="showVersion" v-model="config.showVersion"></checkbox>
|
||||
<p class="comment">选中后,在界面中显示系统版本号。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>定制版本号</td>
|
||||
<td>
|
||||
<input type="text" name="version" v-model="config.version" maxlength="100"/>
|
||||
<p class="comment">定制自己的版本号,留空表示使用系统自带的版本号。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="teaIsPlus">
|
||||
<td>显示模块</td>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">产品名称 *</td>
|
||||
<td>
|
||||
<checkbox name="supportModuleCDN" v-model="supportModuleCDN">CDN</checkbox>
|
||||
<checkbox name="supportModuleNS" v-model="supportModuleNS" v-show="nsIsVisible">智能DNS</checkbox>
|
||||
<p class="comment">当前管理系统中可以显示的模块,不能为空。</p>
|
||||
<input type="text" name="productName" v-model="config.productName" maxlength="100" />
|
||||
<p class="comment">可以使用变量<code-label>${product.name}</code-label>在网页里展示此名称,建议仅包含英文和数字字符。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>管理员系统名称 *</td>
|
||||
<td>
|
||||
<input type="text" name="adminSystemName" v-model="config.adminSystemName" maxlength="100" />
|
||||
<p class="comment">当前管理系统界面上显示的名称。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>显示版本号</td>
|
||||
<td>
|
||||
<checkbox name="showVersion" v-model="config.showVersion"></checkbox>
|
||||
<p class="comment">选中后,在界面中显示系统版本号。</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>定制版本号</td>
|
||||
<td>
|
||||
<input type="text" name="version" v-model="config.version" maxlength="100" />
|
||||
<p class="comment">定制自己的版本号,留空表示使用系统自带的版本号。</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>显示财务相关功能</td>
|
||||
<td>
|
||||
@@ -51,13 +44,14 @@
|
||||
<td>浏览器图标</td>
|
||||
<td>
|
||||
<div v-if="config.faviconFileId > 0">
|
||||
<a :href="'/ui/image/' + config.faviconFileId" target="_blank"><img alt="" :src="'/ui/image/' + config.faviconFileId" style="width:32px;border:1px #ccc solid;"/></a>
|
||||
<a :href="'/ui/image/' + config.faviconFileId" target="_blank"><img alt=""
|
||||
:src="'/ui/image/' + config.faviconFileId" style="width:32px;border:1px #ccc solid;" /></a>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="disabled">还没有上传。</span>
|
||||
</div>
|
||||
<div style="margin-top: 0.8em">
|
||||
<input type="file" name="faviconFile" accept=".png"/>
|
||||
<input type="file" name="faviconFile" accept=".png" />
|
||||
</div>
|
||||
<p class="comment">在浏览器标签栏显示的图标,请使用PNG格式。</p>
|
||||
</td>
|
||||
@@ -66,25 +60,27 @@
|
||||
<td>Logo</td>
|
||||
<td>
|
||||
<div v-if="config.logoFileId > 0">
|
||||
<a :href="'/ui/image/' + config.logoFileId" target="_blank"><img :src="'/ui/image/' + config.logoFileId" style="width:32px;border:1px #ccc solid;"/></a>
|
||||
<a :href="'/ui/image/' + config.logoFileId" target="_blank"><img
|
||||
:src="'/ui/image/' + config.logoFileId" style="width:32px;border:1px #ccc solid;" /></a>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="disabled">还没有上传。</span>
|
||||
</div>
|
||||
<div style="margin-top: 0.8em">
|
||||
<input type="file" name="logoFile" accept=".png"/>
|
||||
<input type="file" name="logoFile" accept=".png" />
|
||||
</div>
|
||||
<p class="comment">显示在系统界面上的图标,请使用PNG格式。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</table>
|
||||
|
||||
<h4>其他</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">每页显示数</td>
|
||||
<td>
|
||||
<input type="text" name="defaultPageSize" v-model="config.defaultPageSize" maxlength="3" style="width: 4em"/>
|
||||
<input type="text" name="defaultPageSize" v-model="config.defaultPageSize" maxlength="3"
|
||||
style="width: 4em" />
|
||||
<p class="comment">在有分页的地方每页显示数量;不能超过100。</p>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -94,12 +90,15 @@
|
||||
<div class="ui fields inline">
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" v-model="timeZoneGroupCode">
|
||||
<option v-for="timeZoneGroup in timeZoneGroups" :value="timeZoneGroup.code">{{timeZoneGroup.name}}</option>
|
||||
<option v-for="timeZoneGroup in timeZoneGroups" :value="timeZoneGroup.code">
|
||||
{{timeZoneGroup.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ui field">
|
||||
<select class="ui dropdown" name="timeZone" v-model="config.timeZone">
|
||||
<option v-for="timeZoneLocation in timeZoneLocations" :value="timeZoneLocation.name" v-if="timeZoneLocation.group == timeZoneGroupCode">{{timeZoneLocation.name}} ({{timeZoneLocation.offset}})</option>
|
||||
<option v-for="timeZoneLocation in timeZoneLocations" :value="timeZoneLocation.name"
|
||||
v-if="timeZoneLocation.group == timeZoneGroupCode">{{timeZoneLocation.name}}
|
||||
({{timeZoneLocation.offset}})</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,5 +118,5 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<submit-btn></submit-btn>
|
||||
<submit-btn></submit-btn>
|
||||
</form>
|
||||
@@ -1,3 +1,139 @@
|
||||
{$layout}
|
||||
|
||||
<p class="comment">此功能暂未开放,敬请期待。</p>
|
||||
<div class="margin"></div>
|
||||
|
||||
<!-- 自动升级设置 -->
|
||||
<div class="ui segment">
|
||||
<h3>自动升级</h3>
|
||||
<table class="ui table definition">
|
||||
<tr>
|
||||
<td class="title">开启自动升级</td>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="autoUpgrade" v-model="config.autoUpgrade" @change="updateAutoUpgrade">
|
||||
<label></label>
|
||||
</div>
|
||||
<p class="comment">开启后,边缘节点、DNS节点、HTTPDNS节点每分钟检查新版本并自动下载升级。关闭后节点不会自动升级,但管理员仍可在下方手动升级。</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="margin"></div>
|
||||
|
||||
<!-- 手动升级 -->
|
||||
<div class="ui segment">
|
||||
<h3 style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>手动升级</span>
|
||||
<button class="ui button primary tiny" v-if="totalUpgradeCount > 0"
|
||||
@click.prevent="upgradeAll()">全部升级({{totalUpgradeCount}})</button>
|
||||
</h3>
|
||||
|
||||
<div v-if="modules.length == 0">
|
||||
<p class="comment">暂无需要升级的节点。</p>
|
||||
</div>
|
||||
|
||||
<div v-for="mod in modules" class="ui segment" style="margin-bottom: 1em; padding: 0;">
|
||||
<h4 style="cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 0.8em 1em; margin: 0; background: #f9fafb; border-bottom: 1px solid rgba(34,36,38,.15);"
|
||||
@click.prevent="mod.expanded = !mod.expanded">
|
||||
<div>
|
||||
<i class="icon angle down" v-if="mod.expanded"></i>
|
||||
<i class="icon angle right" v-else></i>
|
||||
{{mod.name}}
|
||||
<span class="ui label tiny basic" v-if="mod.count > 0"
|
||||
style="margin-left: 0.5em;">{{mod.count}}个待升级</span>
|
||||
</div>
|
||||
<button class="ui button primary tiny" v-if="mod.count > 0"
|
||||
@click.stop.prevent="upgradeModule(mod.code)">升级所有{{mod.name}}</button>
|
||||
</h4>
|
||||
|
||||
<div v-show="mod.expanded" style="padding: 1em;">
|
||||
<div v-for="cluster in mod.clusters" style="margin-bottom: 1em;">
|
||||
<h5 style="cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 0.6em; background: #f3f4f5; border-radius: 4px; margin: 0;"
|
||||
@click.prevent="cluster.expanded = !cluster.expanded">
|
||||
<div>
|
||||
<i class="icon angle down" v-if="cluster.expanded"></i>
|
||||
<i class="icon angle right" v-else></i>
|
||||
{{cluster.name}}
|
||||
<span class="ui label tiny basic" style="margin-left: 0.5em;">{{cluster.count}}个待升级</span>
|
||||
</div>
|
||||
<div>
|
||||
<button class="ui button tiny primary" v-if="countCheckedNodesInCluster(cluster) > 0"
|
||||
@click.stop.prevent="upgradeBatchInCluster(mod.code, cluster)">批量升级({{countCheckedNodesInCluster(cluster)}})</button>
|
||||
<button class="ui button tiny"
|
||||
@click.stop.prevent="upgradeCluster(mod.code, cluster.id)">升级集群内所有节点</button>
|
||||
</div>
|
||||
</h5>
|
||||
|
||||
<div v-show="cluster.expanded" style="margin-top: 0.5em;">
|
||||
<table class="ui table selectable celled small" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:3em">
|
||||
<div class="ui checkbox" @click.prevent="toggleCheckAll(cluster)">
|
||||
<input type="checkbox" :checked="isAllChecked(cluster)">
|
||||
<label></label>
|
||||
</div>
|
||||
</th>
|
||||
<th>节点名</th>
|
||||
<th>访问IP</th>
|
||||
<th>SSH地址</th>
|
||||
<th>版本变化</th>
|
||||
<th class="four wide">节点状态</th>
|
||||
<th class="two op">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr v-for="node in cluster.nodes">
|
||||
<td>
|
||||
<div class="ui checkbox" v-if="!isNodeUpgradeFinished(node)">
|
||||
<input type="checkbox" v-model="node.isChecked">
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{{node.name}}
|
||||
<a :href="nodeDetailURL(mod.code, cluster.id, node.id)" title="节点详情" style="margin-left: 0.4em"><i class="icon external alternate small"></i></a>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="node.accessIP && node.accessIP.length > 0" class="ui label tiny basic">{{node.accessIP}}</span>
|
||||
<span v-else class="disabled">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="node.login != null && node.login.type == 'ssh' && node.loginParams != null && node.loginParams.host != null && node.loginParams.host.length > 0">
|
||||
{{node.loginParams.host}}:{{node.loginParams.port}}
|
||||
</span>
|
||||
<span v-else class="disabled">没有设置</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="node.oldVersion">v{{node.oldVersion}}</span>
|
||||
<span v-else class="ui label tiny">未知</span>
|
||||
-> <strong>v{{node.newVersion}}</strong>
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="node.installStatus != null && (node.installStatus.isRunning || node.installStatus.isFinished)">
|
||||
<div v-if="node.installStatus.isRunning && !node.installStatus.isFinished"
|
||||
class="blue">
|
||||
<i class="notched circle loading icon"></i> 升级中...
|
||||
</div>
|
||||
<div v-if="node.installStatus.isFinished">
|
||||
<span v-if="node.installStatus.isOk" class="green"><i
|
||||
class="icon check circle"></i>
|
||||
已升级成功</span>
|
||||
<span v-if="!node.installStatus.isOk" class="red"><i
|
||||
class="icon warning circle"></i>
|
||||
升级过程中发生错误:{{node.installStatus.error}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="disabled">等待升级</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="" @click.prevent="upgradeNode(mod.code, node)" v-if="!node.isUpgrading">升级</a>
|
||||
<span v-if="node.isUpgrading" class="blue">升级中...</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
457
EdgeAdmin/web/views/@default/settings/upgrade/index.js
Normal file
457
EdgeAdmin/web/views/@default/settings/upgrade/index.js
Normal file
@@ -0,0 +1,457 @@
|
||||
Tea.context(function () {
|
||||
let that = this
|
||||
|
||||
// 计算总待升级数
|
||||
this.totalUpgradeCount = 0
|
||||
this.modules.forEach(function (mod) {
|
||||
mod.expanded = true
|
||||
that.totalUpgradeCount += mod.count
|
||||
if (mod.clusters != null) {
|
||||
mod.clusters.forEach(function (cluster) {
|
||||
cluster.expanded = true
|
||||
if (cluster.nodes != null) {
|
||||
cluster.nodes.forEach(function (node) {
|
||||
node.isUpgrading = false
|
||||
node.isChecked = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 正在升级的节点ID集合(按模块)
|
||||
this.upgradingNodeIds = {
|
||||
"node": [],
|
||||
"dns": [],
|
||||
"httpdns": []
|
||||
}
|
||||
|
||||
// 启动状态轮询
|
||||
this.$delay(function () {
|
||||
this.pollStatus()
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取节点详情页URL
|
||||
*/
|
||||
this.nodeDetailURL = function (moduleCode, clusterId, nodeId) {
|
||||
switch (moduleCode) {
|
||||
case "node":
|
||||
return "/clusters/cluster/node?clusterId=" + clusterId + "&nodeId=" + nodeId
|
||||
case "dns":
|
||||
return "/ns/clusters/cluster/node?clusterId=" + clusterId + "&nodeId=" + nodeId
|
||||
case "httpdns":
|
||||
return "/httpdns/clusters/cluster/node?clusterId=" + clusterId + "&nodeId=" + nodeId
|
||||
default:
|
||||
return "#"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断节点升级是否已完成(成功)
|
||||
*/
|
||||
this.isNodeUpgradeFinished = function (node) {
|
||||
return node.installStatus != null && node.installStatus.isFinished && node.installStatus.isOk
|
||||
}
|
||||
|
||||
/**
|
||||
* 全选/取消全选
|
||||
*/
|
||||
this.toggleCheckAll = function (cluster) {
|
||||
if (cluster.nodes == null) return
|
||||
let allChecked = that.isAllChecked(cluster)
|
||||
cluster.nodes.forEach(function (node) {
|
||||
if (!that.isNodeUpgradeFinished(node)) {
|
||||
node.isChecked = !allChecked
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否全部选中
|
||||
*/
|
||||
this.isAllChecked = function (cluster) {
|
||||
if (cluster.nodes == null || cluster.nodes.length == 0) return false
|
||||
let checkableNodes = cluster.nodes.filter(function (node) {
|
||||
return !that.isNodeUpgradeFinished(node)
|
||||
})
|
||||
if (checkableNodes.length == 0) return false
|
||||
return checkableNodes.every(function (node) { return node.isChecked })
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算集群内选中的节点数
|
||||
*/
|
||||
this.countCheckedNodesInCluster = function (cluster) {
|
||||
if (cluster.nodes == null) return 0
|
||||
return cluster.nodes.filter(function (node) { return node.isChecked }).length
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部升级
|
||||
*/
|
||||
this.upgradeAll = function () {
|
||||
var vue = this
|
||||
teaweb.confirm("确定要升级所有模块的所有待升级节点吗?", function () {
|
||||
vue.$post("/settings/upgrade/upgradeNode")
|
||||
.params({
|
||||
module: "",
|
||||
scope: "all",
|
||||
clusterId: 0,
|
||||
nodeId: 0
|
||||
})
|
||||
.success(function () {
|
||||
// 标记所有节点为升级中
|
||||
that.modules.forEach(function (mod) {
|
||||
if (mod.clusters != null) {
|
||||
mod.clusters.forEach(function (cluster) {
|
||||
if (cluster.nodes != null) {
|
||||
cluster.nodes.forEach(function (node) {
|
||||
node.installStatus = {
|
||||
isRunning: true,
|
||||
isFinished: false,
|
||||
isOk: false,
|
||||
error: "",
|
||||
errorCode: ""
|
||||
}
|
||||
node.isUpgrading = true
|
||||
that.trackNode(mod.code, node.id)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.fail(function (resp) {
|
||||
teaweb.warn("升级请求失败:" + resp.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 按模块升级
|
||||
*/
|
||||
this.upgradeModule = function (moduleCode) {
|
||||
var vue = this
|
||||
teaweb.confirm("确定要升级该模块的所有待升级节点吗?", function () {
|
||||
vue.$post("/settings/upgrade/upgradeNode")
|
||||
.params({
|
||||
module: moduleCode,
|
||||
scope: "module",
|
||||
clusterId: 0,
|
||||
nodeId: 0
|
||||
})
|
||||
.success(function () {
|
||||
that.markModuleUpgrading(moduleCode)
|
||||
})
|
||||
.fail(function (resp) {
|
||||
teaweb.warn("升级请求失败:" + resp.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 按集群升级
|
||||
*/
|
||||
this.upgradeCluster = function (moduleCode, clusterId) {
|
||||
var vue = this
|
||||
teaweb.confirm("确定要升级该集群的所有待升级节点吗?", function () {
|
||||
vue.$post("/settings/upgrade/upgradeNode")
|
||||
.params({
|
||||
module: moduleCode,
|
||||
scope: "cluster",
|
||||
clusterId: clusterId,
|
||||
nodeId: 0
|
||||
})
|
||||
.success(function () {
|
||||
that.markClusterUpgrading(moduleCode, clusterId)
|
||||
})
|
||||
.fail(function (resp) {
|
||||
teaweb.warn("升级请求失败:" + resp.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量升级集群内选中的节点
|
||||
*/
|
||||
this.upgradeBatchInCluster = function (moduleCode, cluster) {
|
||||
if (cluster.nodes == null) return
|
||||
var checkedNodes = cluster.nodes.filter(function (node) { return node.isChecked })
|
||||
if (checkedNodes.length == 0) return
|
||||
|
||||
var vue = this
|
||||
teaweb.confirm("确定要批量升级选中的 " + checkedNodes.length + " 个节点吗?", function () {
|
||||
checkedNodes.forEach(function (node) {
|
||||
node.installStatus = {
|
||||
isRunning: true,
|
||||
isFinished: false,
|
||||
isOk: false,
|
||||
error: "",
|
||||
errorCode: ""
|
||||
}
|
||||
node.isUpgrading = true
|
||||
node.isChecked = false
|
||||
that.trackNode(moduleCode, node.id)
|
||||
|
||||
vue.$post("/settings/upgrade/upgradeNode")
|
||||
.params({
|
||||
module: moduleCode,
|
||||
scope: "node",
|
||||
clusterId: 0,
|
||||
nodeId: node.id
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 升级单个节点
|
||||
*/
|
||||
this.upgradeNode = function (moduleCode, node) {
|
||||
var vue = this
|
||||
teaweb.confirm("确定要升级节点 \"" + node.name + "\" 吗?", function () {
|
||||
node.installStatus = {
|
||||
isRunning: true,
|
||||
isFinished: false,
|
||||
isOk: false,
|
||||
error: "",
|
||||
errorCode: ""
|
||||
}
|
||||
node.isUpgrading = true
|
||||
that.trackNode(moduleCode, node.id)
|
||||
|
||||
vue.$post("/settings/upgrade/upgradeNode")
|
||||
.params({
|
||||
module: moduleCode,
|
||||
scope: "node",
|
||||
clusterId: 0,
|
||||
nodeId: node.id
|
||||
})
|
||||
.success(function () { })
|
||||
.fail(function (resp) {
|
||||
node.isUpgrading = false
|
||||
teaweb.warn("升级请求失败:" + resp.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记模块下所有节点为升级中
|
||||
*/
|
||||
this.markModuleUpgrading = function (moduleCode) {
|
||||
that.modules.forEach(function (mod) {
|
||||
if (mod.code == moduleCode && mod.clusters != null) {
|
||||
mod.clusters.forEach(function (cluster) {
|
||||
if (cluster.nodes != null) {
|
||||
cluster.nodes.forEach(function (node) {
|
||||
node.installStatus = {
|
||||
isRunning: true,
|
||||
isFinished: false,
|
||||
isOk: false,
|
||||
error: "",
|
||||
errorCode: ""
|
||||
}
|
||||
node.isUpgrading = true
|
||||
that.trackNode(moduleCode, node.id)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记集群下所有节点为升级中
|
||||
*/
|
||||
this.markClusterUpgrading = function (moduleCode, clusterId) {
|
||||
that.modules.forEach(function (mod) {
|
||||
if (mod.code == moduleCode && mod.clusters != null) {
|
||||
mod.clusters.forEach(function (cluster) {
|
||||
if (cluster.id == clusterId && cluster.nodes != null) {
|
||||
cluster.nodes.forEach(function (node) {
|
||||
node.installStatus = {
|
||||
isRunning: true,
|
||||
isFinished: false,
|
||||
isOk: false,
|
||||
error: "",
|
||||
errorCode: ""
|
||||
}
|
||||
node.isUpgrading = true
|
||||
that.trackNode(moduleCode, node.id)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪节点升级状态
|
||||
*/
|
||||
this.trackNode = function (moduleCode, nodeId) {
|
||||
if (that.upgradingNodeIds[moduleCode] == null) {
|
||||
that.upgradingNodeIds[moduleCode] = []
|
||||
}
|
||||
if (that.upgradingNodeIds[moduleCode].indexOf(nodeId) < 0) {
|
||||
that.upgradingNodeIds[moduleCode].push(nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态轮询
|
||||
*/
|
||||
this.pollStatus = function () {
|
||||
var vue = this
|
||||
|
||||
// 检查是否有正在升级的节点
|
||||
let hasUpgrading = false
|
||||
for (let key in that.upgradingNodeIds) {
|
||||
if (that.upgradingNodeIds[key].length > 0) {
|
||||
hasUpgrading = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasUpgrading) {
|
||||
setTimeout(function () { that.pollStatus() }, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
vue.$post("/settings/upgrade/status")
|
||||
.params({
|
||||
nodeIdsJSON: JSON.stringify(that.upgradingNodeIds)
|
||||
})
|
||||
.success(function (resp) {
|
||||
let statuses = resp.data.statuses
|
||||
if (statuses == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新各模块节点状态
|
||||
for (let moduleCode in statuses) {
|
||||
let nodeStatuses = statuses[moduleCode]
|
||||
if (nodeStatuses == null) {
|
||||
continue
|
||||
}
|
||||
nodeStatuses.forEach(function (ns) {
|
||||
that.updateNodeStatus(moduleCode, ns.id, ns.installStatus)
|
||||
})
|
||||
}
|
||||
})
|
||||
.done(function () {
|
||||
setTimeout(function () { that.pollStatus() }, 3000)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新节点安装状态
|
||||
*/
|
||||
this.updateNodeStatus = function (moduleCode, nodeId, installStatus) {
|
||||
that.modules.forEach(function (mod) {
|
||||
if (mod.code == moduleCode && mod.clusters != null) {
|
||||
mod.clusters.forEach(function (cluster) {
|
||||
if (cluster.nodes != null) {
|
||||
cluster.nodes.forEach(function (node) {
|
||||
if (node.id == nodeId && installStatus != null) {
|
||||
node.installStatus = installStatus
|
||||
// 升级完成后移除跟踪
|
||||
if (installStatus.isFinished) {
|
||||
node.isUpgrading = false
|
||||
let idx = that.upgradingNodeIds[moduleCode].indexOf(nodeId)
|
||||
if (idx >= 0) {
|
||||
that.upgradingNodeIds[moduleCode].splice(idx, 1)
|
||||
}
|
||||
|
||||
if (installStatus.isOk) {
|
||||
// 升级成功,延迟后从列表中移除
|
||||
setTimeout(function () {
|
||||
let nIdx = cluster.nodes.indexOf(node)
|
||||
if (nIdx >= 0) {
|
||||
cluster.nodes.splice(nIdx, 1)
|
||||
cluster.count--
|
||||
mod.count--
|
||||
that.totalUpgradeCount--
|
||||
|
||||
if (cluster.count <= 0) {
|
||||
let cIdx = mod.clusters.indexOf(cluster)
|
||||
if (cIdx >= 0) {
|
||||
mod.clusters.splice(cIdx, 1)
|
||||
}
|
||||
}
|
||||
if (mod.count <= 0) {
|
||||
let mIdx = that.modules.indexOf(mod)
|
||||
if (mIdx >= 0) {
|
||||
that.modules.splice(mIdx, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 2000)
|
||||
} else {
|
||||
// 升级失败,根据 errorCode 给出具体提示
|
||||
that.handleUpgradeError(moduleCode, node, installStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理升级错误,根据 errorCode 提供更友好的提示
|
||||
*/
|
||||
this.handleUpgradeError = function (moduleCode, node, installStatus) {
|
||||
let errorCode = installStatus.errorCode || ""
|
||||
let errMsg = installStatus.error || ""
|
||||
|
||||
switch (errorCode) {
|
||||
case "EMPTY_LOGIN":
|
||||
case "EMPTY_SSH_HOST":
|
||||
case "EMPTY_SSH_PORT":
|
||||
case "EMPTY_GRANT":
|
||||
// SSH 信息未配置的错误,不弹窗(页面上已有"没有设置"提示)
|
||||
break
|
||||
case "CREATE_ROOT_DIRECTORY_FAILED":
|
||||
teaweb.warn("节点 \"" + node.name + "\" 创建根目录失败:" + errMsg)
|
||||
break
|
||||
case "INSTALL_HELPER_FAILED":
|
||||
teaweb.warn("节点 \"" + node.name + "\" 安装助手失败:" + errMsg)
|
||||
break
|
||||
case "TEST_FAILED":
|
||||
teaweb.warn("节点 \"" + node.name + "\" 环境测试失败:" + errMsg)
|
||||
break
|
||||
case "RPC_TEST_FAILED":
|
||||
teaweb.warn("节点 \"" + node.name + "\" RPC通讯测试失败:" + errMsg)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新自动升级状态
|
||||
*/
|
||||
this.updateAutoUpgrade = function () {
|
||||
var vue = this
|
||||
|
||||
// @change 已经翻转了值,先记录新值并立即恢复
|
||||
let newValue = that.config.autoUpgrade
|
||||
that.config.autoUpgrade = !newValue
|
||||
|
||||
let msg = newValue ? "确定要开启自动升级功能吗?开启后节点会自动下载安装新版本。" : "确定要关闭自动升级功能吗?关闭后只能通过这里手动执行升级。"
|
||||
teaweb.confirm(msg, function () {
|
||||
vue.$post("/settings/upgrade")
|
||||
.params({
|
||||
autoUpgrade: newValue ? 1 : 0
|
||||
})
|
||||
.success(function () {
|
||||
that.config.autoUpgrade = newValue
|
||||
teaweb.successToast("设置保存成功")
|
||||
})
|
||||
.fail(function (resp) {
|
||||
teaweb.warn("设置保存失败:" + resp.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -39,7 +39,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="hasHTTPDNSFeature">
|
||||
<td>HTTPDNS关联集群</td>
|
||||
<td style="white-space: nowrap">HTTPDNS关联集群</td>
|
||||
<td>
|
||||
<select class="ui dropdown auto-width" name="httpdnsClusterId" v-model="httpdnsClusterId">
|
||||
<option value="0">[未选择]</option>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
.ui.table.definition td.title {
|
||||
min-width: 12em;
|
||||
}
|
||||
.feature-boxes .feature-box {
|
||||
margin-bottom: 1em;
|
||||
width: 24em;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">允许注册</td>
|
||||
<td class="title" style="white-space: nowrap">允许注册</td>
|
||||
<td>
|
||||
<checkbox name="isOn" v-model="config.isOn"></checkbox>
|
||||
<p class="comment">选中表示允许用户自行注册。</p>
|
||||
@@ -40,7 +40,7 @@
|
||||
<h4>登录设置</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">检查客户端区域</td>
|
||||
<td class="title" style="white-space: nowrap">检查客户端区域</td>
|
||||
<td>
|
||||
<checkbox name="checkClientRegion" v-model="config.checkClientRegion"></checkbox>
|
||||
<p class="comment">选中后,表示每次用户访问时都检查客户端所在地理区域是否和登录时一致,以提升安全性;如果当前系统下游有反向代理设置,请在<a
|
||||
@@ -53,7 +53,7 @@
|
||||
<h4>电子邮箱相关</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">启用电子邮箱绑定功能</td>
|
||||
<td class="title" style="white-space: nowrap">启用电子邮箱绑定功能</td>
|
||||
<td>
|
||||
<checkbox name="emailVerificationIsOn" v-model="config.emailVerification.isOn"></checkbox>
|
||||
<p class="comment">选中后,电子邮箱需要激活之后可以使用邮箱登录、找回密码等。此功能需要事先设置 <a href="/users/setting/email"
|
||||
@@ -107,7 +107,7 @@
|
||||
<h4>通过邮箱找回密码</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">启用找回密码功能</td>
|
||||
<td class="title" style="white-space: nowrap">启用找回密码功能</td>
|
||||
<td>
|
||||
<checkbox name="emailResetPasswordIsOn" v-model="config.emailResetPassword.isOn"></checkbox>
|
||||
<p class="comment">选中后,用户可以通过已绑定的电子邮箱找回密码;此功能需要同时开启电子邮箱绑定功能。</p>
|
||||
@@ -146,7 +146,7 @@
|
||||
<h4>手机号码相关</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">启用手机号码绑定功能</td>
|
||||
<td class="title" style="white-space: nowrap">启用手机号码绑定功能</td>
|
||||
<td>
|
||||
<checkbox name="mobileVerificationIsOn" v-model="config.mobileVerification.isOn"></checkbox>
|
||||
<p class="comment">选中后,手机号码需要激活之后可以使用手机号码、找回密码等。此功能需要事先设置 <a href="/users/setting/sms"
|
||||
@@ -196,7 +196,7 @@
|
||||
<h4>CDN服务</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">开通CDN服务</td>
|
||||
<td class="title" style="white-space: nowrap">开通CDN服务</td>
|
||||
<td>
|
||||
<checkbox name="cdnIsOn" v-model="config.cdnIsOn"></checkbox>
|
||||
<p class="comment">选中表示自动为用户开通CDN服务。 </p>
|
||||
@@ -254,7 +254,7 @@
|
||||
<h4>DDoS高防</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">开通DDoS高防管理</td>
|
||||
<td class="title" style="white-space: nowrap">开通DDoS高防管理</td>
|
||||
<td>
|
||||
<checkbox name="adIsOn" v-model="config.adIsOn"></checkbox>
|
||||
<p class="comment">选中表示自动为用户开通DDoS高防IP使用服务。</p>
|
||||
@@ -269,7 +269,7 @@
|
||||
<h4>智能DNS服务</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">开通智能DNS服务</td>
|
||||
<td class="title" style="white-space: nowrap">开通智能DNS服务</td>
|
||||
<td>
|
||||
<checkbox name="nsIsOn" v-model="config.nsIsOn"></checkbox>
|
||||
<p class="comment">选中表示自动为用户开通智能DNS服务。使用默认集群资源,如需使用其他集群,请到用户新增和业务设置页面中进行选择。</p>
|
||||
@@ -284,7 +284,7 @@
|
||||
<h4>HTTPDNS服务</h4>
|
||||
<table class="ui table definition selectable">
|
||||
<tr>
|
||||
<td class="title">开通HTTPDNS服务</td>
|
||||
<td class="title" style="white-space: nowrap">开通HTTPDNS服务</td>
|
||||
<td>
|
||||
<checkbox name="httpdnsIsOn" v-model="config.httpdnsIsOn"></checkbox>
|
||||
<p class="comment">选中表示自动为用户开通HTTPDNS服务。使用默认集群资源,如需使用其他集群,请到用户新增和业务设置页面中进行选择。</p>
|
||||
|
||||
@@ -7,9 +7,7 @@ import (
|
||||
"github.com/oschwald/geoip2-golang"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MaxMindReader MaxMind GeoIP2 Reader
|
||||
@@ -19,6 +17,9 @@ type MaxMindReader struct {
|
||||
meta *Meta
|
||||
initialized bool
|
||||
mutex sync.RWMutex
|
||||
|
||||
// 临时文件路径,Destroy 时自动清理
|
||||
tmpFiles []string
|
||||
}
|
||||
|
||||
// NewMaxMindReader 创建 MaxMind Reader
|
||||
@@ -66,27 +67,9 @@ func NewMaxMindReaderFromBytes(cityDBData, asnDBData []byte) (*MaxMindReader, er
|
||||
return nil, fmt.Errorf("city database data is required")
|
||||
}
|
||||
|
||||
// 创建临时文件,使用更唯一的文件名避免冲突
|
||||
tmpDir := os.TempDir()
|
||||
pid := os.Getpid()
|
||||
// 使用时间戳增加唯一性,避免同一进程多次调用时的冲突
|
||||
timestamp := time.Now().UnixNano()
|
||||
cityTmpFile := filepath.Join(tmpDir, fmt.Sprintf("geolite2-city-%d-%d.mmdb", pid, timestamp))
|
||||
asnTmpFile := filepath.Join(tmpDir, fmt.Sprintf("geolite2-asn-%d-%d.mmdb", pid, timestamp))
|
||||
|
||||
// 如果临时文件已存在,先删除(可能是之前崩溃留下的)
|
||||
os.Remove(cityTmpFile)
|
||||
os.Remove(asnTmpFile)
|
||||
|
||||
// 写入 City 数据库到临时文件
|
||||
if err := os.WriteFile(cityTmpFile, cityDBData, 0644); err != nil {
|
||||
return nil, fmt.Errorf("write city database to temp file failed: %w", err)
|
||||
}
|
||||
|
||||
// 打开 City 数据库
|
||||
db, err := geoip2.Open(cityTmpFile)
|
||||
// 直接从内存字节加载,避免在 /tmp 持续生成 mmdb 临时文件。
|
||||
db, err := geoip2.FromBytes(cityDBData)
|
||||
if err != nil {
|
||||
os.Remove(cityTmpFile)
|
||||
return nil, fmt.Errorf("open MaxMind city database failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -94,16 +77,11 @@ func NewMaxMindReaderFromBytes(cityDBData, asnDBData []byte) (*MaxMindReader, er
|
||||
db: db,
|
||||
}
|
||||
|
||||
// 写入并打开 ASN 数据库(可选)
|
||||
// 加载 ASN 数据库(可选)
|
||||
if len(asnDBData) > 0 {
|
||||
if err := os.WriteFile(asnTmpFile, asnDBData, 0644); err == nil {
|
||||
dbASN, err := geoip2.Open(asnTmpFile)
|
||||
if err == nil {
|
||||
reader.dbASN = dbASN
|
||||
} else {
|
||||
// ASN 数据库打开失败,清理临时文件但不影响主功能
|
||||
os.Remove(asnTmpFile)
|
||||
}
|
||||
dbASN, err := geoip2.FromBytes(asnDBData)
|
||||
if err == nil {
|
||||
reader.dbASN = dbASN
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +152,7 @@ func (this *MaxMindReader) Meta() *Meta {
|
||||
return this.meta
|
||||
}
|
||||
|
||||
// Destroy 销毁 Reader
|
||||
// Destroy 销毁 Reader 并清理临时文件
|
||||
func (this *MaxMindReader) Destroy() {
|
||||
this.mutex.Lock()
|
||||
defer this.mutex.Unlock()
|
||||
@@ -187,6 +165,10 @@ func (this *MaxMindReader) Destroy() {
|
||||
this.dbASN.Close()
|
||||
this.dbASN = nil
|
||||
}
|
||||
for _, f := range this.tmpFiles {
|
||||
os.Remove(f)
|
||||
}
|
||||
this.tmpFiles = nil
|
||||
this.initialized = false
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ type HTTPDNSCluster struct {
|
||||
UpdatedAt int64 `protobuf:"varint,11,opt,name=updatedAt,proto3" json:"updatedAt,omitempty"`
|
||||
AutoRemoteStart bool `protobuf:"varint,12,opt,name=autoRemoteStart,proto3" json:"autoRemoteStart,omitempty"`
|
||||
AccessLogIsOn bool `protobuf:"varint,13,opt,name=accessLogIsOn,proto3" json:"accessLogIsOn,omitempty"`
|
||||
TimeZone string `protobuf:"bytes,14,opt,name=timeZone,proto3" json:"timeZone,omitempty"`
|
||||
}
|
||||
|
||||
func (x *HTTPDNSCluster) Reset() {
|
||||
@@ -161,6 +162,13 @@ func (x *HTTPDNSCluster) GetAccessLogIsOn() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *HTTPDNSCluster) GetTimeZone() string {
|
||||
if x != nil {
|
||||
return x.TimeZone
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_models_model_httpdns_cluster_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_models_model_httpdns_cluster_proto_rawDesc = []byte{
|
||||
|
||||
@@ -35,6 +35,7 @@ type CreateHTTPDNSClusterRequest struct {
|
||||
IsDefault bool `protobuf:"varint,8,opt,name=isDefault,proto3" json:"isDefault,omitempty"`
|
||||
AutoRemoteStart bool `protobuf:"varint,9,opt,name=autoRemoteStart,proto3" json:"autoRemoteStart,omitempty"`
|
||||
AccessLogIsOn bool `protobuf:"varint,10,opt,name=accessLogIsOn,proto3" json:"accessLogIsOn,omitempty"`
|
||||
TimeZone string `protobuf:"bytes,11,opt,name=timeZone,proto3" json:"timeZone,omitempty"`
|
||||
}
|
||||
|
||||
func (x *CreateHTTPDNSClusterRequest) Reset() {
|
||||
@@ -137,6 +138,13 @@ func (x *CreateHTTPDNSClusterRequest) GetAccessLogIsOn() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *CreateHTTPDNSClusterRequest) GetTimeZone() string {
|
||||
if x != nil {
|
||||
return x.TimeZone
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type CreateHTTPDNSClusterResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -198,6 +206,7 @@ type UpdateHTTPDNSClusterRequest struct {
|
||||
IsDefault bool `protobuf:"varint,9,opt,name=isDefault,proto3" json:"isDefault,omitempty"`
|
||||
AutoRemoteStart bool `protobuf:"varint,10,opt,name=autoRemoteStart,proto3" json:"autoRemoteStart,omitempty"`
|
||||
AccessLogIsOn bool `protobuf:"varint,11,opt,name=accessLogIsOn,proto3" json:"accessLogIsOn,omitempty"`
|
||||
TimeZone string `protobuf:"bytes,12,opt,name=timeZone,proto3" json:"timeZone,omitempty"`
|
||||
}
|
||||
|
||||
func (x *UpdateHTTPDNSClusterRequest) Reset() {
|
||||
@@ -307,6 +316,13 @@ func (x *UpdateHTTPDNSClusterRequest) GetAccessLogIsOn() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *UpdateHTTPDNSClusterRequest) GetTimeZone() string {
|
||||
if x != nil {
|
||||
return x.TimeZone
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type DeleteHTTPDNSClusterRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
||||
@@ -27,7 +27,10 @@ const (
|
||||
HTTPDNSNodeService_UpdateHTTPDNSNodeStatus_FullMethodName = "/pb.HTTPDNSNodeService/updateHTTPDNSNodeStatus"
|
||||
HTTPDNSNodeService_UpdateHTTPDNSNodeLogin_FullMethodName = "/pb.HTTPDNSNodeService/updateHTTPDNSNodeLogin"
|
||||
HTTPDNSNodeService_CheckHTTPDNSNodeLatestVersion_FullMethodName = "/pb.HTTPDNSNodeService/checkHTTPDNSNodeLatestVersion"
|
||||
HTTPDNSNodeService_DownloadHTTPDNSNodeInstallationFile_FullMethodName = "/pb.HTTPDNSNodeService/downloadHTTPDNSNodeInstallationFile"
|
||||
HTTPDNSNodeService_DownloadHTTPDNSNodeInstallationFile_FullMethodName = "/pb.HTTPDNSNodeService/downloadHTTPDNSNodeInstallationFile"
|
||||
HTTPDNSNodeService_CountAllUpgradeHTTPDNSNodesWithClusterId_FullMethodName = "/pb.HTTPDNSNodeService/countAllUpgradeHTTPDNSNodesWithClusterId"
|
||||
HTTPDNSNodeService_FindAllUpgradeHTTPDNSNodesWithClusterId_FullMethodName = "/pb.HTTPDNSNodeService/findAllUpgradeHTTPDNSNodesWithClusterId"
|
||||
HTTPDNSNodeService_UpgradeHTTPDNSNode_FullMethodName = "/pb.HTTPDNSNodeService/upgradeHTTPDNSNode"
|
||||
)
|
||||
|
||||
// HTTPDNSNodeServiceClient is the client API for HTTPDNSNodeService service.
|
||||
@@ -46,6 +49,12 @@ type HTTPDNSNodeServiceClient interface {
|
||||
CheckHTTPDNSNodeLatestVersion(ctx context.Context, in *CheckHTTPDNSNodeLatestVersionRequest, opts ...grpc.CallOption) (*CheckHTTPDNSNodeLatestVersionResponse, error)
|
||||
// 下载最新HTTPDNS节点安装文件
|
||||
DownloadHTTPDNSNodeInstallationFile(ctx context.Context, in *DownloadHTTPDNSNodeInstallationFileRequest, opts ...grpc.CallOption) (*DownloadHTTPDNSNodeInstallationFileResponse, error)
|
||||
// 计算需要升级的HTTPDNS节点数量
|
||||
CountAllUpgradeHTTPDNSNodesWithClusterId(ctx context.Context, in *CountAllUpgradeHTTPDNSNodesWithClusterIdRequest, opts ...grpc.CallOption) (*RPCCountResponse, error)
|
||||
// 列出所有需要升级的HTTPDNS节点
|
||||
FindAllUpgradeHTTPDNSNodesWithClusterId(ctx context.Context, in *FindAllUpgradeHTTPDNSNodesWithClusterIdRequest, opts ...grpc.CallOption) (*FindAllUpgradeHTTPDNSNodesWithClusterIdResponse, error)
|
||||
// 升级单个HTTPDNS节点
|
||||
UpgradeHTTPDNSNode(ctx context.Context, in *UpgradeHTTPDNSNodeRequest, opts ...grpc.CallOption) (*RPCSuccess, error)
|
||||
}
|
||||
|
||||
type hTTPDNSNodeServiceClient struct {
|
||||
@@ -146,6 +155,36 @@ func (c *hTTPDNSNodeServiceClient) DownloadHTTPDNSNodeInstallationFile(ctx conte
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *hTTPDNSNodeServiceClient) CountAllUpgradeHTTPDNSNodesWithClusterId(ctx context.Context, in *CountAllUpgradeHTTPDNSNodesWithClusterIdRequest, opts ...grpc.CallOption) (*RPCCountResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RPCCountResponse)
|
||||
err := c.cc.Invoke(ctx, HTTPDNSNodeService_CountAllUpgradeHTTPDNSNodesWithClusterId_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *hTTPDNSNodeServiceClient) FindAllUpgradeHTTPDNSNodesWithClusterId(ctx context.Context, in *FindAllUpgradeHTTPDNSNodesWithClusterIdRequest, opts ...grpc.CallOption) (*FindAllUpgradeHTTPDNSNodesWithClusterIdResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(FindAllUpgradeHTTPDNSNodesWithClusterIdResponse)
|
||||
err := c.cc.Invoke(ctx, HTTPDNSNodeService_FindAllUpgradeHTTPDNSNodesWithClusterId_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *hTTPDNSNodeServiceClient) UpgradeHTTPDNSNode(ctx context.Context, in *UpgradeHTTPDNSNodeRequest, opts ...grpc.CallOption) (*RPCSuccess, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RPCSuccess)
|
||||
err := c.cc.Invoke(ctx, HTTPDNSNodeService_UpgradeHTTPDNSNode_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// HTTPDNSNodeServiceServer is the server API for HTTPDNSNodeService service.
|
||||
// All implementations must embed UnimplementedHTTPDNSNodeServiceServer
|
||||
// for forward compatibility.
|
||||
@@ -162,6 +201,12 @@ type HTTPDNSNodeServiceServer interface {
|
||||
CheckHTTPDNSNodeLatestVersion(context.Context, *CheckHTTPDNSNodeLatestVersionRequest) (*CheckHTTPDNSNodeLatestVersionResponse, error)
|
||||
// 下载最新HTTPDNS节点安装文件
|
||||
DownloadHTTPDNSNodeInstallationFile(context.Context, *DownloadHTTPDNSNodeInstallationFileRequest) (*DownloadHTTPDNSNodeInstallationFileResponse, error)
|
||||
// 计算需要升级的HTTPDNS节点数量
|
||||
CountAllUpgradeHTTPDNSNodesWithClusterId(context.Context, *CountAllUpgradeHTTPDNSNodesWithClusterIdRequest) (*RPCCountResponse, error)
|
||||
// 列出所有需要升级的HTTPDNS节点
|
||||
FindAllUpgradeHTTPDNSNodesWithClusterId(context.Context, *FindAllUpgradeHTTPDNSNodesWithClusterIdRequest) (*FindAllUpgradeHTTPDNSNodesWithClusterIdResponse, error)
|
||||
// 升级单个HTTPDNS节点
|
||||
UpgradeHTTPDNSNode(context.Context, *UpgradeHTTPDNSNodeRequest) (*RPCSuccess, error)
|
||||
mustEmbedUnimplementedHTTPDNSNodeServiceServer()
|
||||
}
|
||||
|
||||
@@ -199,6 +244,15 @@ func (UnimplementedHTTPDNSNodeServiceServer) CheckHTTPDNSNodeLatestVersion(conte
|
||||
func (UnimplementedHTTPDNSNodeServiceServer) DownloadHTTPDNSNodeInstallationFile(context.Context, *DownloadHTTPDNSNodeInstallationFileRequest) (*DownloadHTTPDNSNodeInstallationFileResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method DownloadHTTPDNSNodeInstallationFile not implemented")
|
||||
}
|
||||
func (UnimplementedHTTPDNSNodeServiceServer) CountAllUpgradeHTTPDNSNodesWithClusterId(context.Context, *CountAllUpgradeHTTPDNSNodesWithClusterIdRequest) (*RPCCountResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CountAllUpgradeHTTPDNSNodesWithClusterId not implemented")
|
||||
}
|
||||
func (UnimplementedHTTPDNSNodeServiceServer) FindAllUpgradeHTTPDNSNodesWithClusterId(context.Context, *FindAllUpgradeHTTPDNSNodesWithClusterIdRequest) (*FindAllUpgradeHTTPDNSNodesWithClusterIdResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method FindAllUpgradeHTTPDNSNodesWithClusterId not implemented")
|
||||
}
|
||||
func (UnimplementedHTTPDNSNodeServiceServer) UpgradeHTTPDNSNode(context.Context, *UpgradeHTTPDNSNodeRequest) (*RPCSuccess, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UpgradeHTTPDNSNode not implemented")
|
||||
}
|
||||
func (UnimplementedHTTPDNSNodeServiceServer) mustEmbedUnimplementedHTTPDNSNodeServiceServer() {}
|
||||
func (UnimplementedHTTPDNSNodeServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
@@ -382,6 +436,60 @@ func _HTTPDNSNodeService_DownloadHTTPDNSNodeInstallationFile_Handler(srv interfa
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _HTTPDNSNodeService_CountAllUpgradeHTTPDNSNodesWithClusterId_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CountAllUpgradeHTTPDNSNodesWithClusterIdRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(HTTPDNSNodeServiceServer).CountAllUpgradeHTTPDNSNodesWithClusterId(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: HTTPDNSNodeService_CountAllUpgradeHTTPDNSNodesWithClusterId_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(HTTPDNSNodeServiceServer).CountAllUpgradeHTTPDNSNodesWithClusterId(ctx, req.(*CountAllUpgradeHTTPDNSNodesWithClusterIdRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _HTTPDNSNodeService_FindAllUpgradeHTTPDNSNodesWithClusterId_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(FindAllUpgradeHTTPDNSNodesWithClusterIdRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(HTTPDNSNodeServiceServer).FindAllUpgradeHTTPDNSNodesWithClusterId(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: HTTPDNSNodeService_FindAllUpgradeHTTPDNSNodesWithClusterId_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(HTTPDNSNodeServiceServer).FindAllUpgradeHTTPDNSNodesWithClusterId(ctx, req.(*FindAllUpgradeHTTPDNSNodesWithClusterIdRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _HTTPDNSNodeService_UpgradeHTTPDNSNode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(UpgradeHTTPDNSNodeRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(HTTPDNSNodeServiceServer).UpgradeHTTPDNSNode(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: HTTPDNSNodeService_UpgradeHTTPDNSNode_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(HTTPDNSNodeServiceServer).UpgradeHTTPDNSNode(ctx, req.(*UpgradeHTTPDNSNodeRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// HTTPDNSNodeService_ServiceDesc is the grpc.ServiceDesc for HTTPDNSNodeService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -425,6 +533,18 @@ var HTTPDNSNodeService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "downloadHTTPDNSNodeInstallationFile",
|
||||
Handler: _HTTPDNSNodeService_DownloadHTTPDNSNodeInstallationFile_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "countAllUpgradeHTTPDNSNodesWithClusterId",
|
||||
Handler: _HTTPDNSNodeService_CountAllUpgradeHTTPDNSNodesWithClusterId_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "findAllUpgradeHTTPDNSNodesWithClusterId",
|
||||
Handler: _HTTPDNSNodeService_FindAllUpgradeHTTPDNSNodesWithClusterId_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "upgradeHTTPDNSNode",
|
||||
Handler: _HTTPDNSNodeService_UpgradeHTTPDNSNode_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "service_httpdns_node.proto",
|
||||
|
||||
@@ -46,6 +46,8 @@ const (
|
||||
NSNodeService_UpdateNSNodeDDoSProtection_FullMethodName = "/pb.NSNodeService/updateNSNodeDDoSProtection"
|
||||
NSNodeService_FindNSNodeAPIConfig_FullMethodName = "/pb.NSNodeService/findNSNodeAPIConfig"
|
||||
NSNodeService_UpdateNSNodeAPIConfig_FullMethodName = "/pb.NSNodeService/updateNSNodeAPIConfig"
|
||||
NSNodeService_FindAllUpgradeNSNodesWithNSClusterId_FullMethodName = "/pb.NSNodeService/findAllUpgradeNSNodesWithNSClusterId"
|
||||
NSNodeService_UpgradeNSNode_FullMethodName = "/pb.NSNodeService/upgradeNSNode"
|
||||
)
|
||||
|
||||
// NSNodeServiceClient is the client API for NSNodeService service.
|
||||
@@ -108,6 +110,10 @@ type NSNodeServiceClient interface {
|
||||
FindNSNodeAPIConfig(ctx context.Context, in *FindNSNodeAPIConfigRequest, opts ...grpc.CallOption) (*FindNSNodeAPIConfigResponse, error)
|
||||
// 修改某个节点的API相关配置
|
||||
UpdateNSNodeAPIConfig(ctx context.Context, in *UpdateNSNodeAPIConfigRequest, opts ...grpc.CallOption) (*RPCSuccess, error)
|
||||
// 列出所有需要升级的NS节点
|
||||
FindAllUpgradeNSNodesWithNSClusterId(ctx context.Context, in *FindAllUpgradeNSNodesWithNSClusterIdRequest, opts ...grpc.CallOption) (*FindAllUpgradeNSNodesWithNSClusterIdResponse, error)
|
||||
// 升级单个NS节点
|
||||
UpgradeNSNode(ctx context.Context, in *UpgradeNSNodeRequest, opts ...grpc.CallOption) (*RPCSuccess, error)
|
||||
}
|
||||
|
||||
type nSNodeServiceClient struct {
|
||||
@@ -391,6 +397,26 @@ func (c *nSNodeServiceClient) UpdateNSNodeAPIConfig(ctx context.Context, in *Upd
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *nSNodeServiceClient) FindAllUpgradeNSNodesWithNSClusterId(ctx context.Context, in *FindAllUpgradeNSNodesWithNSClusterIdRequest, opts ...grpc.CallOption) (*FindAllUpgradeNSNodesWithNSClusterIdResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(FindAllUpgradeNSNodesWithNSClusterIdResponse)
|
||||
err := c.cc.Invoke(ctx, NSNodeService_FindAllUpgradeNSNodesWithNSClusterId_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *nSNodeServiceClient) UpgradeNSNode(ctx context.Context, in *UpgradeNSNodeRequest, opts ...grpc.CallOption) (*RPCSuccess, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RPCSuccess)
|
||||
err := c.cc.Invoke(ctx, NSNodeService_UpgradeNSNode_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// NSNodeServiceServer is the server API for NSNodeService service.
|
||||
// All implementations should embed UnimplementedNSNodeServiceServer
|
||||
// for forward compatibility.
|
||||
@@ -451,6 +477,10 @@ type NSNodeServiceServer interface {
|
||||
FindNSNodeAPIConfig(context.Context, *FindNSNodeAPIConfigRequest) (*FindNSNodeAPIConfigResponse, error)
|
||||
// 修改某个节点的API相关配置
|
||||
UpdateNSNodeAPIConfig(context.Context, *UpdateNSNodeAPIConfigRequest) (*RPCSuccess, error)
|
||||
// 列出所有需要升级的NS节点
|
||||
FindAllUpgradeNSNodesWithNSClusterId(context.Context, *FindAllUpgradeNSNodesWithNSClusterIdRequest) (*FindAllUpgradeNSNodesWithNSClusterIdResponse, error)
|
||||
// 升级单个NS节点
|
||||
UpgradeNSNode(context.Context, *UpgradeNSNodeRequest) (*RPCSuccess, error)
|
||||
}
|
||||
|
||||
// UnimplementedNSNodeServiceServer should be embedded to have
|
||||
@@ -541,6 +571,12 @@ func (UnimplementedNSNodeServiceServer) FindNSNodeAPIConfig(context.Context, *Fi
|
||||
func (UnimplementedNSNodeServiceServer) UpdateNSNodeAPIConfig(context.Context, *UpdateNSNodeAPIConfigRequest) (*RPCSuccess, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateNSNodeAPIConfig not implemented")
|
||||
}
|
||||
func (UnimplementedNSNodeServiceServer) FindAllUpgradeNSNodesWithNSClusterId(context.Context, *FindAllUpgradeNSNodesWithNSClusterIdRequest) (*FindAllUpgradeNSNodesWithNSClusterIdResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method FindAllUpgradeNSNodesWithNSClusterId not implemented")
|
||||
}
|
||||
func (UnimplementedNSNodeServiceServer) UpgradeNSNode(context.Context, *UpgradeNSNodeRequest) (*RPCSuccess, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpgradeNSNode not implemented")
|
||||
}
|
||||
func (UnimplementedNSNodeServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeNSNodeServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
@@ -1036,6 +1072,42 @@ func _NSNodeService_UpdateNSNodeAPIConfig_Handler(srv interface{}, ctx context.C
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _NSNodeService_FindAllUpgradeNSNodesWithNSClusterId_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(FindAllUpgradeNSNodesWithNSClusterIdRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(NSNodeServiceServer).FindAllUpgradeNSNodesWithNSClusterId(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: NSNodeService_FindAllUpgradeNSNodesWithNSClusterId_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(NSNodeServiceServer).FindAllUpgradeNSNodesWithNSClusterId(ctx, req.(*FindAllUpgradeNSNodesWithNSClusterIdRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _NSNodeService_UpgradeNSNode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(UpgradeNSNodeRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(NSNodeServiceServer).UpgradeNSNode(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: NSNodeService_UpgradeNSNode_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(NSNodeServiceServer).UpgradeNSNode(ctx, req.(*UpgradeNSNodeRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// NSNodeService_ServiceDesc is the grpc.ServiceDesc for NSNodeService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -1147,6 +1219,14 @@ var NSNodeService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "updateNSNodeAPIConfig",
|
||||
Handler: _NSNodeService_UpdateNSNodeAPIConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "findAllUpgradeNSNodesWithNSClusterId",
|
||||
Handler: _NSNodeService_FindAllUpgradeNSNodesWithNSClusterId_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "upgradeNSNode",
|
||||
Handler: _NSNodeService_UpgradeNSNode_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
|
||||
@@ -17,4 +17,5 @@ message HTTPDNSCluster {
|
||||
int64 updatedAt = 11;
|
||||
bool autoRemoteStart = 12;
|
||||
bool accessLogIsOn = 13;
|
||||
string timeZone = 14;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ message CreateHTTPDNSClusterRequest {
|
||||
bool isDefault = 8;
|
||||
bool autoRemoteStart = 9;
|
||||
bool accessLogIsOn = 10;
|
||||
string timeZone = 11;
|
||||
}
|
||||
|
||||
message CreateHTTPDNSClusterResponse {
|
||||
@@ -47,6 +48,7 @@ message UpdateHTTPDNSClusterRequest {
|
||||
bool isDefault = 9;
|
||||
bool autoRemoteStart = 10;
|
||||
bool accessLogIsOn = 11;
|
||||
string timeZone = 12;
|
||||
}
|
||||
|
||||
message DeleteHTTPDNSClusterRequest {
|
||||
|
||||
@@ -22,6 +22,15 @@ service HTTPDNSNodeService {
|
||||
|
||||
// 下载最新HTTPDNS节点安装文件
|
||||
rpc downloadHTTPDNSNodeInstallationFile (DownloadHTTPDNSNodeInstallationFileRequest) returns (DownloadHTTPDNSNodeInstallationFileResponse);
|
||||
|
||||
// 计算需要升级的HTTPDNS节点数量
|
||||
rpc countAllUpgradeHTTPDNSNodesWithClusterId (CountAllUpgradeHTTPDNSNodesWithClusterIdRequest) returns (RPCCountResponse);
|
||||
|
||||
// 列出所有需要升级的HTTPDNS节点
|
||||
rpc findAllUpgradeHTTPDNSNodesWithClusterId (FindAllUpgradeHTTPDNSNodesWithClusterIdRequest) returns (FindAllUpgradeHTTPDNSNodesWithClusterIdResponse);
|
||||
|
||||
// 升级单个HTTPDNS节点
|
||||
rpc upgradeHTTPDNSNode (UpgradeHTTPDNSNodeRequest) returns (RPCSuccess);
|
||||
}
|
||||
|
||||
message CreateHTTPDNSNodeRequest {
|
||||
@@ -103,3 +112,30 @@ message DownloadHTTPDNSNodeInstallationFileResponse {
|
||||
string version = 4;
|
||||
string filename = 5;
|
||||
}
|
||||
|
||||
// 计算需要升级的HTTPDNS节点数量
|
||||
message CountAllUpgradeHTTPDNSNodesWithClusterIdRequest {
|
||||
int64 clusterId = 1;
|
||||
}
|
||||
|
||||
// 列出所有需要升级的HTTPDNS节点
|
||||
message FindAllUpgradeHTTPDNSNodesWithClusterIdRequest {
|
||||
int64 clusterId = 1;
|
||||
}
|
||||
|
||||
message FindAllUpgradeHTTPDNSNodesWithClusterIdResponse {
|
||||
repeated HTTPDNSNodeUpgrade nodes = 1;
|
||||
|
||||
message HTTPDNSNodeUpgrade {
|
||||
HTTPDNSNode node = 1;
|
||||
string os = 2;
|
||||
string arch = 3;
|
||||
string oldVersion = 4;
|
||||
string newVersion = 5;
|
||||
}
|
||||
}
|
||||
|
||||
// 升级单个HTTPDNS节点
|
||||
message UpgradeHTTPDNSNodeRequest {
|
||||
int64 nodeId = 1;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ service NSNodeService {
|
||||
// 计算需要升级的NS节点数量
|
||||
rpc countAllUpgradeNSNodesWithNSClusterId (CountAllUpgradeNSNodesWithNSClusterIdRequest) returns (RPCCountResponse);
|
||||
|
||||
// 列出所有需要升级的NS节点
|
||||
rpc findAllUpgradeNSNodesWithNSClusterId (FindAllUpgradeNSNodesWithNSClusterIdRequest) returns (FindAllUpgradeNSNodesWithNSClusterIdResponse);
|
||||
|
||||
// 升级单个NS节点
|
||||
rpc upgradeNSNode (UpgradeNSNodeRequest) returns (RPCSuccess);
|
||||
|
||||
// 创建NS节点
|
||||
rpc createNSNode (CreateNSNodeRequest) returns (CreateNSNodeResponse);
|
||||
|
||||
@@ -316,4 +322,26 @@ message FindNSNodeAPIConfigResponse {
|
||||
message UpdateNSNodeAPIConfigRequest {
|
||||
int64 nsNodeId = 1;
|
||||
bytes apiNodeAddrsJSON = 2;
|
||||
}
|
||||
|
||||
// 列出所有需要升级的NS节点
|
||||
message FindAllUpgradeNSNodesWithNSClusterIdRequest {
|
||||
int64 nsClusterId = 1;
|
||||
}
|
||||
|
||||
message FindAllUpgradeNSNodesWithNSClusterIdResponse {
|
||||
repeated NSNodeUpgrade nodes = 1;
|
||||
|
||||
message NSNodeUpgrade {
|
||||
NSNode nsNode = 1;
|
||||
string os = 2;
|
||||
string arch = 3;
|
||||
string oldVersion = 4;
|
||||
string newVersion = 5;
|
||||
}
|
||||
}
|
||||
|
||||
// 升级单个NS节点
|
||||
message UpgradeNSNodeRequest {
|
||||
int64 nsNodeId = 1;
|
||||
}
|
||||
@@ -17,4 +17,5 @@ const (
|
||||
SettingCodeStandaloneInstanceInitialized SettingCode = "standaloneInstanceInitialized" // 单体实例初始化状态
|
||||
|
||||
SettingCodeHTTPDNSDefaultBackupClusterId SettingCode = "httpdnsDefaultBackupClusterId" // HTTPDNS默认备用集群ID
|
||||
SettingCodeUpgradeConfig SettingCode = "upgradeConfig" // 升级设置
|
||||
)
|
||||
|
||||
@@ -58,7 +58,7 @@ func (this *UserFeature) ToPB() *pb.UserFeature {
|
||||
func FindAllUserFeatures() []*UserFeature {
|
||||
return []*UserFeature{
|
||||
{
|
||||
Name: "记录访问日志",
|
||||
Name: "访问日志记录",
|
||||
Code: UserFeatureCodeServerAccessLog,
|
||||
Description: "用户可以开启服务的访问日志。",
|
||||
SupportPlan: true,
|
||||
@@ -106,7 +106,7 @@ func FindAllUserFeatures() []*UserFeature {
|
||||
SupportPlan: false,
|
||||
},
|
||||
{
|
||||
Name: "开启WAF",
|
||||
Name: "WAF",
|
||||
Code: UserFeatureCodeServerWAF,
|
||||
Description: "用户可以开启WAF功能并可以设置黑白名单等。",
|
||||
SupportPlan: true,
|
||||
@@ -136,7 +136,7 @@ func FindAllUserFeatures() []*UserFeature {
|
||||
SupportPlan: true,
|
||||
},
|
||||
{
|
||||
Name: "页面优化",
|
||||
Name: "页面动态加密",
|
||||
Code: UserFeatureCodeServerOptimization,
|
||||
Description: "用户可以开启页面优化功能。",
|
||||
SupportPlan: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package teaconst
|
||||
|
||||
const (
|
||||
Version = "1.4.8" //1.3.8.2
|
||||
Version = "1.4.9" //1.3.8.2
|
||||
|
||||
ProductName = "Edge DNS"
|
||||
ProcessName = "edge-dns"
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
1.4.8版本Changelog(HTTPDNS功能全量发布)
|
||||
|
||||
1、通过智能解析把用户就近调度到最优节点,显著降低首包时延与连接抖动。
|
||||
2、支持按地域、运营商、国内/海外精细分流,提升弱网与跨网访问稳定性。
|
||||
3、解析链路内置签名鉴权与请求追踪,增强安全性与可观测性。
|
||||
4、无命中规则时自动回源兜底,保障解析连续可用。
|
||||
5、支持 A/AAAA 双栈与多记录返回,兼容不同终端网络环境。
|
||||
6、提供Android、iOS、Flutter 多端SDK 开箱可用,支持预解析、缓存与同步/非阻塞解析。
|
||||
7、提供 IP 直连适配能力(含 Host 保留与 No-SNI 模式),适配复杂 HTTPS 场景。
|
||||
8、控制台支持应用/域名/规则全流程配置与在线验证,缩短问题定位和发布周期。
|
||||
9、节点支持在线安装升级与日志上报,降低运维复杂度并提升可维护性。
|
||||
|
||||
1.4.8版本升级步骤
|
||||
|
||||
1、备份现有配置与数据:
|
||||
将 edge-api、edge-admin、edge-user 等组件目录下的 configs 文件夹,以及平台的 MySQL 数据库进行全量备份;
|
||||
|
||||
2、停止旧版本进程(管理端各组件):
|
||||
killall -9 edge-api
|
||||
killall -9 edge-admin
|
||||
killall -9 edge-user
|
||||
|
||||
3、上传并解压新版本包(以 Linux x64 环境为例):
|
||||
unzip -o edge-admin-linux-amd64-v1.4.8.zip -d /data/
|
||||
unzip -o edge-user-linux-amd64-v1.4.8.zip -d /data/
|
||||
|
||||
4、依次运行edge-api、edge-admin、edge-user
|
||||
|
||||
# 启动 API 服务
|
||||
cd /data/edge-api/bin
|
||||
chmod +x edge-api
|
||||
nohup ./edge-api 2>&1 &
|
||||
|
||||
# 启动管理后台
|
||||
cd /data/edge-admin/bin
|
||||
chmod +x edge-admin
|
||||
nohup ./edge-admin 2>&1 &
|
||||
|
||||
# 启动租户控制台
|
||||
cd /data/edge-user/bin
|
||||
chmod +x edge-user
|
||||
nohup ./edge-user 2>&1 &
|
||||
|
||||
5、检查版本状态:
|
||||
登录管理后台,确认系统版本显示为 1.4.8;
|
||||
|
||||
6、配置主备集群:
|
||||
进入“HTTPDNS -> 集群列表 -> 集群设置”,按需勾选“默认主集群”或“默认备用集群”角色,以便后续应用自动关联;
|
||||
|
||||
7、在线升级 HTTPDNS 节点:
|
||||
进入“HTTPDNS -> 节点列表”,点击对应节点的“详情”,在“安装信息”页面点击 **[在线安装]** 或 **[升级]**。系统会自动下发最新的 edge-httpdns 二进制文件并完成重启。
|
||||
|
||||
8、验证节点在线状态:
|
||||
等待 30 秒左右,确认节点状态恢复为“在线”,并验证硬件负载监控数据是否正常上报。
|
||||
|
||||
9、业务解析验证:
|
||||
使用控制台“解析测试”工具,验证域名在当前环境下是否能正确返回调度的 IP 地址。
|
||||
|
||||
10、完成升级。
|
||||
|
||||
特别说明:
|
||||
1、在线升级模式:Edge HTTPDNS 节点支持通过管理平台一键在线升级,无需手动上传文件和重启进程。
|
||||
2、离线安装模式:如节点服务器无法连接控制台,可手动上传 edge-httpdns 压缩包并解压,更新 bin 目录下的程序文件后手动执行 `./edge-httpdns restart` 即可。
|
||||
3、SNI 隐匿功能:请确保关联的 CDN 边缘节点也已同步更新至配套版本(会自动升级)。
|
||||
@@ -1,147 +0,0 @@
|
||||
# HTTPDNS 主计划(V1.2 现行设计)
|
||||
|
||||
## 1. 目标
|
||||
1. 构建独立的 HTTPDNS 管理闭环:集群、应用、域名、自定义解析、访问日志、运行日志、解析测试。
|
||||
2. 方案以当前已确认的页面与交互为准,不保留历史分支设计。
|
||||
3. 优先保证可运维、可灰度、可观测,先落地稳定版本。
|
||||
|
||||
## 2. 信息架构(菜单)
|
||||
1. 左侧 HTTPDNS 菜单顺序:
|
||||
- 集群管理
|
||||
- 应用管理
|
||||
- 访问日志
|
||||
- 运行日志
|
||||
- 解析测试
|
||||
2. 不再保留独立“全局配置”菜单。
|
||||
3. 不再保留独立“SDK接入引导”菜单。
|
||||
|
||||
## 3. 核心设计约束
|
||||
1. SNI 防护策略固定为“隐匿 SNI”(不提供 level1/level3、mask/empty切换入口)。
|
||||
2. 服务入口按“集群服务域名”管理,不使用全局单入口。
|
||||
3. 回源协议默认 HTTPS,不提供开关。
|
||||
4. 域名校验默认开启,不在用户侧暴露开关。
|
||||
5. 自定义解析先做精简版:不支持 SDNS 参数匹配。
|
||||
|
||||
## 4. 集群管理
|
||||
|
||||
### 4.1 集群列表
|
||||
1. 字段:集群名称、服务域名、节点数、在线节点数、状态、操作。
|
||||
2. 操作:节点列表、集群设置、删除集群。
|
||||
|
||||
### 4.2 节点列表
|
||||
1. 字段:节点名称、IP、CPU、内存、负载、状态、操作。
|
||||
2. IP 列不展示“[下线]/[宕机]”附加标识。
|
||||
|
||||
### 4.3 集群设置
|
||||
1. 页面布局对齐智能DNS:左侧菜单 + 右侧配置区(`left-box with-menu` / `right-box with-menu`)。
|
||||
2. 配置分组:
|
||||
- 基础设置
|
||||
- TLS
|
||||
3. 基础设置字段:
|
||||
- 集群名称
|
||||
- 服务域名
|
||||
- 默认解析 TTL(秒)
|
||||
- 降级超时容忍度(毫秒)
|
||||
- 节点安装根目录(默认 `/opt/edge-httpdns`)
|
||||
- 启用当前集群
|
||||
- 默认集群(勾选)
|
||||
4. TLS 配置:
|
||||
- 样式与交互对齐智能DNS TLS页
|
||||
- 维护并绑定该集群服务域名使用的证书
|
||||
- 保证节点回源链路为 HTTPS
|
||||
|
||||
## 5. 应用管理
|
||||
|
||||
### 5.1 应用列表
|
||||
1. 字段:应用名称、AppID、绑定域名数、状态、操作。
|
||||
2. 操作:域名列表、应用设置、SDK集成、删除应用。
|
||||
3. 删除应用页面交互风格对齐“删除集群”页面。
|
||||
|
||||
### 5.2 应用设置
|
||||
1. 页面布局对齐智能DNS设置页风格(左侧菜单 + 右侧配置区)。
|
||||
2. 分组:
|
||||
- 基础配置
|
||||
- 认证与密钥
|
||||
3. 基础配置字段:
|
||||
- AppID(只读)
|
||||
- 主集群
|
||||
- 备集群(可选)
|
||||
- 应用启用
|
||||
- SNI 防护配置(文案展示:隐匿 SNI)
|
||||
4. 认证与密钥字段:
|
||||
- 请求验签(状态展示 + 独立启停按钮 + 确认提示)
|
||||
- 加签 Secret(查看、复制、重置、最近更新时间)
|
||||
5. 交互约束:
|
||||
- 强制 HTTPS 传输,不再提供独立“数据加密 Secret”配置项。
|
||||
- 密钥操作区使用紧凑图标样式,减少视觉噪音。
|
||||
|
||||
## 6. 域名管理与自定义解析
|
||||
|
||||
### 6.1 域名管理
|
||||
1. 页面采用框架标准顶部 tab + 面包屑样式。
|
||||
2. 字段:
|
||||
- 域名列表
|
||||
- 规则策略(仅展示数字,表示该域名规则数,可点击)
|
||||
- 操作(自定义解析、解绑)
|
||||
3. 入口:在域名行操作中直接进入“自定义解析”。
|
||||
|
||||
### 6.2 自定义解析(精简版)
|
||||
1. 规则字段:
|
||||
- 规则名称
|
||||
- 线路
|
||||
- 解析记录值
|
||||
- TTL
|
||||
- 状态
|
||||
2. 线路联动:
|
||||
- 第一层:`中国地区` / `境外`
|
||||
- 中国地区:运营商 -> 大区 -> 省份
|
||||
- 境外:洲 -> 国家/地区(亚洲内使用中国香港/中国澳门/中国台湾)
|
||||
3. 解析记录:
|
||||
- 每条规则最多 10 条
|
||||
- 支持 A / AAAA
|
||||
- 可开启权重调度
|
||||
4. 不包含 SDNS 参数配置。
|
||||
|
||||
## 7. 访问日志
|
||||
1. 菜单与页面文案统一为“访问日志”(不再使用“解析日志”)。
|
||||
2. 页面结构:筛选区与列表区分离,间距与智能DNS访问日志风格一致。
|
||||
3. 列字段:集群、节点、域名、类型、概要。
|
||||
4. 概要展示:
|
||||
- 使用单行拼接
|
||||
- 按“访问信息 -> 解析结果”顺序
|
||||
- 不显示字段名堆叠
|
||||
|
||||
## 8. 运行日志
|
||||
1. 字段与智能DNS运行日志保持一致。
|
||||
2. 级别样式使用文字着色,不使用大块背景色。
|
||||
|
||||
## 9. 解析测试
|
||||
1. 去掉“API在线沙盒”标题与右侧等待占位图标区。
|
||||
2. 解析配置区标题统一为“解析测试”。
|
||||
3. 测试参数保留:目标应用、所属集群、解析域名、模拟客户端IP、解析类型(A/AAAA)。
|
||||
4. 所属集群使用下拉框选择。
|
||||
5. 解析域名使用下拉框选择(按当前目标应用联动展示)。
|
||||
6. 去掉 SDNS 参数。
|
||||
7. 结果区保留核心结果展示,参考阿里云风格的简洁结果布局。
|
||||
|
||||
## 10. SDK 集成
|
||||
1. SDK 集成不作为左侧独立菜单。
|
||||
2. 在“应用管理”操作列进入 SDK 集成页。
|
||||
3. 页面只保留:
|
||||
- SDK 下载
|
||||
- 集成文档
|
||||
4. 卡片与按钮采用紧凑布局,避免按钮区域拥挤。
|
||||
|
||||
## 11. SDK 与服务端接口(现行)
|
||||
1. 解析接口:`/resolve`
|
||||
- SDK 请求域名解析结果的主接口。
|
||||
2. 启动配置接口:`/bootstrap`(规划/联调口径)
|
||||
- 用于 SDK 获取可用服务域名与策略参数(替代节点IP调度模式)。
|
||||
3. SDK 地址策略:优先主集群服务域名,不可用时切备集群;失败后按客户端降级策略处理。
|
||||
|
||||
## 12. 本文明确不包含
|
||||
1. 不包含全局配置独立页面设计。
|
||||
2. 不包含 SDK 接入向导独立菜单设计。
|
||||
3. 不包含 SNI level1/level3、ECH 控制台、Public SNI 池分级配置。
|
||||
4. 不包含 SDNS 参数匹配链路。
|
||||
5. 不包含第三方 DNS 依赖与复用方案。
|
||||
@@ -1,255 +0,0 @@
|
||||
# HTTPDNS后端开发计划(V1.0)
|
||||
|
||||
## 0. 文档信息
|
||||
- 目标文件:`EdgeHttpDNS/HTTPDNS后端开发计划.md`
|
||||
- 交付范围:`EdgeAdmin + EdgeAPI + EdgeHttpDNS + SDK对接接口`
|
||||
- 交付策略:一次性全量交付(非分阶段)
|
||||
- 核心约束:
|
||||
- 仅新协议
|
||||
- 无 `/bootstrap`
|
||||
- `/resolve` 使用 GET 参数
|
||||
- 线路匹配仅基于客户端 IP 归属
|
||||
- 独立 `edgeHTTPDNS*` 数据表
|
||||
- 新增独立节点角色 `NodeRoleHTTPDNS`
|
||||
- 访问日志 MySQL + ClickHouse 双写(查询优先 ClickHouse)
|
||||
- 不复用智能DNS(EdgeDNS/NS)模块的 DAO/Service/Store,HTTPDNS 独立实现(可参考并复制所需能力)
|
||||
|
||||
## 1. 目标与成功标准
|
||||
1. 将 HTTPDNS 从当前 Admin 侧 mock/store 方案落地为真实后端能力。
|
||||
2. 打通“配置 -> 下发 -> 解析 -> 日志 -> 查询”闭环。
|
||||
3. 与当前前端设计严格对齐:
|
||||
- 菜单:集群管理、应用管理、访问日志、运行日志、解析测试
|
||||
- SNI 固定为“隐匿 SNI”
|
||||
- 自定义解析不含 SDNS 参数
|
||||
4. 成功标准:
|
||||
- 管理页面均通过 RPC 读取真实数据,无本地 mock 依赖
|
||||
- `/resolve` 可按应用/域名/线路返回解析结果
|
||||
- 访问日志与运行日志可查询、可筛选、可分页
|
||||
- 节点配置与状态可下发和回传
|
||||
- 主备集群服务域名可在应用设置中配置并生效
|
||||
- EdgeHttpDNS 支持 SNI 与 Host 解耦路由,并可执行 WAF 动态验签与隐匿 SNI 转发
|
||||
|
||||
## 2. 架构与边界
|
||||
### 2.1 服务边界
|
||||
1. EdgeAdmin:仅负责页面动作与 RPC 编排,不存业务状态。
|
||||
2. EdgeAPI:负责数据存储、策略匹配、接口服务、日志汇聚。
|
||||
3. EdgeHttpDNS(HTTPDNS节点):负责执行解析、上报运行日志/访问日志、接收任务。
|
||||
4. SDK:手动配置应用关联主备服务域名,调用 `/resolve` 获取结果。
|
||||
|
||||
### 2.2 不做项
|
||||
1. 不做 `/bootstrap` 接口。
|
||||
2. 不做 ECH / SNI 分级策略。
|
||||
3. 不做 SDNS 参数匹配。
|
||||
4. 不做第三方 DNS 依赖复用。
|
||||
|
||||
## 3. 公开接口与契约(需新增/调整)
|
||||
### 3.1 HTTP 解析接口
|
||||
1. `GET /resolve`
|
||||
2. 请求参数:
|
||||
- `appId`(必填)
|
||||
- `dn`(必填)
|
||||
- `qtype`(可选,A/AAAA,默认 A)
|
||||
- `cip`(可选)
|
||||
- `sid`、`sdk_version`、`os`(可选,用于日志)
|
||||
- `nonce`、`exp`、`sign`(可选,验签开启时必需)
|
||||
3. 响应结构(统一):
|
||||
- `code`、`message`、`requestId`
|
||||
- `data`:
|
||||
- `domain`
|
||||
- `qtype`
|
||||
- `ttl`
|
||||
- `records[]`(`type`,`ip`,`weight?`,`line?`,`region?`)
|
||||
- `client`(`ip`,`region`,`carrier`,`country`)
|
||||
- `summary`(命中规则摘要)
|
||||
4. 错误码最低集合:
|
||||
- app 无效/禁用
|
||||
- 域名未绑定
|
||||
- 验签失败
|
||||
- 无可用解析记录
|
||||
- 内部解析失败/超时
|
||||
|
||||
### 3.2 管理 RPC(EdgeAdmin -> EdgeAPI)
|
||||
1. 新增服务:
|
||||
- `HTTPDNSClusterService`
|
||||
- `HTTPDNSNodeService`
|
||||
- `HTTPDNSAppService`
|
||||
- `HTTPDNSDomainService`
|
||||
- `HTTPDNSRuleService`
|
||||
- `HTTPDNSAccessLogService`
|
||||
- `HTTPDNSRuntimeLogService`
|
||||
- `HTTPDNSSandboxService`
|
||||
2. 最小方法集:
|
||||
- 集群:增删改查、设置默认集群、TLS证书绑定、节点列表/状态
|
||||
- 应用:增删改查、主备集群设置、启停、验签开关、密钥重置
|
||||
- 域名:绑定/解绑/列表
|
||||
- 自定义解析:规则增删改查、启停、排序
|
||||
- 日志:访问日志分页查询、运行日志分页查询
|
||||
- 测试:在线解析测试调用(入参包含 appId、clusterId、domain、qtype、clientIp)
|
||||
|
||||
### 3.3 节点日志上报 RPC(EdgeHttpDNS -> EdgeAPI)
|
||||
1. `CreateHTTPDNSAccessLogs`(批量)
|
||||
2. `CreateHTTPDNSRuntimeLogs`(批量)
|
||||
3. 幂等键:`requestId + nodeId`
|
||||
4. 支持高吞吐批量提交和失败重试
|
||||
|
||||
### 3.4 节点路由与 WAF 策略下发契约(EdgeAPI -> EdgeHttpDNS)
|
||||
1. 下发内容最小集合:
|
||||
- `appId/domain/serviceDomain`
|
||||
- `sniMode`(固定为隐匿 SNI)
|
||||
- `hostRouteMode`(SNI 与 Host 解耦)
|
||||
- `wafVerifyEnabled`
|
||||
- `wafVerifyPolicy`(验签字段、时效窗口、失败动作)
|
||||
2. 节点执行口径:
|
||||
- 入站按 `serviceDomain` 接入,按 Host/业务域名做路由匹配
|
||||
- TLS 握手与业务 Host 解耦,避免真实业务域名暴露在 SNI
|
||||
- 命中需要验签的应用时,先执行 WAF 动态验签,再继续解析链路
|
||||
3. 失败处置:
|
||||
- 验签失败按策略拒绝并记录运行日志、访问日志错误码
|
||||
- 路由未命中返回统一错误码并上报审计日志
|
||||
|
||||
## 4. 数据模型设计(独立 HTTPDNS 表)
|
||||
1. `edgeHTTPDNSClusters`
|
||||
- `id,name,isOn,isDefault,serviceDomain,defaultTTL,fallbackTimeoutMs,installDir,tlsPolicyJSON,createdAt,updatedAt`
|
||||
2. `edgeHTTPDNSNodes`
|
||||
- `id,clusterId,name,isOn,isUp,isInstalled,isActive,statusJSON,installStatusJSON,installDir,uniqueId,secret,createdAt,updatedAt`
|
||||
3. `edgeHTTPDNSApps`
|
||||
- `id,name,appId,isOn,primaryClusterId,backupClusterId,sniMode(fixed_hide),createdAt,updatedAt`
|
||||
4. `edgeHTTPDNSAppSecrets`
|
||||
- `id,appId,signEnabled,signSecretEnc,signUpdatedAt,updatedAt`
|
||||
5. `edgeHTTPDNSDomains`
|
||||
- `id,appId,domain,isOn,createdAt,updatedAt`
|
||||
6. `edgeHTTPDNSCustomRules`
|
||||
- `id,appId,domainId,ruleName,lineScope,lineCarrier,lineRegion,lineProvince,lineContinent,lineCountry,ttl,isOn,priority,updatedAt`
|
||||
7. `edgeHTTPDNSCustomRuleRecords`
|
||||
- `id,ruleId,recordType,recordValue,weight,sort`
|
||||
8. `edgeHTTPDNSAccessLogs`
|
||||
- `id,requestId,clusterId,nodeId,appId,domain,qtype,clientIP,clientRegion,carrier,sdkVersion,os,resultIPs,status,errorCode,costMs,createdAt,day`
|
||||
9. `edgeHTTPDNSRuntimeLogs`
|
||||
- `id,clusterId,nodeId,level,type,tag,description,count,createdAt,day`
|
||||
|
||||
### 4.1 索引与唯一约束
|
||||
1. 唯一:`edgeHTTPDNSApps.appId`
|
||||
2. 唯一:`edgeHTTPDNSDomains(appId,domain)`
|
||||
3. 唯一:`edgeHTTPDNSAccessLogs(requestId,nodeId)`
|
||||
4. 索引:
|
||||
- 访问日志:`day,clusterId,nodeId,domain,status,createdAt`
|
||||
- 规则匹配:`domainId,isOn,priority,lineScope,...`
|
||||
- 应用查询:`name,appId,isOn`
|
||||
|
||||
## 5. 解析引擎实现(EdgeAPI)
|
||||
1. 输入校验:`appId/dn/qtype`
|
||||
2. 应用与域名校验:
|
||||
- app 存在且启用
|
||||
- 域名已绑定到 app
|
||||
3. 线路归属:
|
||||
- `cip` 优先,其次 remote IP
|
||||
- 映射字段:运营商/大区/省份/洲/国家
|
||||
4. 规则匹配:
|
||||
- 精确匹配 > 半精确 > 默认
|
||||
- 同级按 `priority` 从小到大
|
||||
5. 记录返回:
|
||||
- 权重关闭:返回全部记录
|
||||
- 权重开启:按权重算法返回单条或子集(固定口径)
|
||||
6. TTL 取值:
|
||||
- 命中规则取规则 TTL
|
||||
- 未命中规则取集群默认 TTL
|
||||
7. 验签:
|
||||
- `signEnabled=true` 时必须通过签名校验
|
||||
- `signEnabled=false` 时跳过签名校验
|
||||
8. 访问日志:
|
||||
- 解析结束异步写日志
|
||||
- 双写 MySQL/ClickHouse
|
||||
|
||||
## 6. 节点与任务链路
|
||||
1. 在 `EdgeCommon/pkg/nodeconfigs` 增加 `NodeRoleHTTPDNS`。
|
||||
2. 在任务系统增加 HTTPDNS 任务类型:
|
||||
- 配置变更
|
||||
- 应用变更
|
||||
- 域名变更
|
||||
- 规则变更
|
||||
- 证书变更
|
||||
- 路由与 WAF 策略变更
|
||||
3. EdgeHttpDNS 增加 HTTPDNS 子服务:
|
||||
- 接收配置快照
|
||||
- 执行解析
|
||||
- 上报运行/访问日志
|
||||
- 执行 SNI/Host 解耦路由
|
||||
- 执行 WAF 动态验签
|
||||
- 执行隐匿 SNI 转发
|
||||
4. 复用现有安装升级框架,但节点角色、任务通道、日志 tag 独立。
|
||||
|
||||
## 7. EdgeAdmin 后端改造
|
||||
1. 将 `internal/web/actions/default/httpdns/*` 的 store/mock 读写改为 RPC。
|
||||
2. 删除/停用旧能力路由:
|
||||
- `/httpdns/policies`
|
||||
- `/httpdns/guide`
|
||||
- `/httpdns/ech`
|
||||
3. 保留必要跳转,避免旧链接 404。
|
||||
4. `sandbox/test` 改为调用真实解析服务(可保留测试开关)。
|
||||
5. 解析测试页面交互固定为:
|
||||
- 配置区标题“解析测试”
|
||||
- 所属集群使用下拉框
|
||||
- 解析域名使用下拉框并按目标应用联动
|
||||
|
||||
## 8. 安全与审计
|
||||
1. Secret 持久化使用加密存储(至少密文列),返回时脱敏。
|
||||
2. 操作审计记录:
|
||||
- 验签开关启停
|
||||
- Sign Secret 重置
|
||||
- 应用主备集群修改
|
||||
3. 验签失败日志保留 `requestId,errorCode,sourceIP`。
|
||||
4. 防滥用:
|
||||
- `/resolve` 增加基础限流与异常请求过滤(按 appId + IP 维度)。
|
||||
5. 节点侧安全执行:
|
||||
- WAF 动态验签失败必须留存审计日志(含 requestId/appId/sourceIP)
|
||||
- SNI/Host 解耦路由命中结果需可追踪(用于问题回溯)
|
||||
|
||||
## 9. 测试与验收
|
||||
### 9.1 单元测试
|
||||
1. 规则匹配优先级与线路匹配
|
||||
2. 验签成功/失败路径
|
||||
3. 权重返回算法
|
||||
4. DAO CRUD 与唯一约束
|
||||
|
||||
### 9.2 集成测试
|
||||
1. EdgeAdmin -> RPC -> DB 全链路
|
||||
2. `/resolve` 各错误码分支
|
||||
3. 节点日志上报双写(MySQL+CH)
|
||||
4. CH 不可用时 MySQL 回退查询
|
||||
5. EdgeAPI 策略下发 -> EdgeHttpDNS 路由/WAF 执行 -> 日志落库全链路
|
||||
|
||||
### 9.3 回归测试
|
||||
1. 智能DNS功能不受影响
|
||||
2. 菜单与权限不串模块
|
||||
3. 旧入口跳转正确,无新增 404
|
||||
|
||||
### 9.4 验收用例
|
||||
1. 应用配置主备服务域名后,SDK可解析成功
|
||||
2. 主集群故障时可切备集群
|
||||
3. 自定义解析按线路返回预期 IP
|
||||
4. 访问日志筛选与概要展示正确
|
||||
5. 运行日志级别/字段与智能DNS一致
|
||||
6. EdgeHttpDNS 可在 SNI 与 Host 解耦场景下正确路由到目标应用
|
||||
7. 开启 WAF 动态验签后,合法请求通过、非法签名请求被拒绝且有审计日志
|
||||
|
||||
## 10. 发布与回滚
|
||||
1. 发布顺序:
|
||||
- DB migration
|
||||
- EdgeAPI(DAO+RPC+resolve)
|
||||
- EdgeHttpDNS(角色+上报)
|
||||
- EdgeAdmin(RPC切换)
|
||||
2. 开关控制:
|
||||
- `httpdns.resolve.enabled`
|
||||
- `httpdns.log.clickhouse.enabled`
|
||||
3. 回滚策略:
|
||||
- 关闭 `resolve` 新实现开关
|
||||
- 访问日志读取切回 MySQL
|
||||
- Admin 保留只读能力
|
||||
|
||||
## 11. 默认值与固定决策
|
||||
1. 无 `/bootstrap`,SDK手动配置主备服务域名。
|
||||
2. `SNI` 固定“隐匿 SNI”。
|
||||
3. 自定义解析无 SDNS 参数。
|
||||
4. 线路仅客户端 IP 归属。
|
||||
5. 日志双写,查询优先 ClickHouse。
|
||||
6. 节点角色独立:`NodeRoleHTTPDNS`。
|
||||
@@ -1,98 +0,0 @@
|
||||
# Edge HTTPDNS 用户使用手册
|
||||
|
||||
欢迎使用 Edge HTTPDNS 服务。本文档旨在帮助您快速完成应用创建、域名配置及解析测试,实现精准、安全的业务调度。
|
||||
|
||||
---
|
||||
|
||||
## 1. 快速入门流程
|
||||
1. **创建应用**:获取接入凭证(AppId 和 SecretKey)。
|
||||
2. **添加域名**:登记需要通过 HTTPDNS 解析的业务域名。
|
||||
3. **自定义解析规则**:设置域名对应的 IP 地址及智能分流规则。
|
||||
4. **解析测试**:通过沙箱工具验证解析是否生效。
|
||||
5. **集成 SDK**:将解析功能集成至您的 App 中。
|
||||
|
||||
---
|
||||
|
||||
## 2. 应用管理
|
||||
|
||||
应用是您接入 HTTPDNS 的基础单元。
|
||||
|
||||
### 2.1 创建应用
|
||||
1. 登录用户控制台,点击 **HTTPDNS -> 应用列表**。
|
||||
2. 点击 **创建应用**。
|
||||
3. 填写应用名称(如“我的安卓App”)。
|
||||
4. 系统会自动关联默认的服务域名,无需手动选择。
|
||||
|
||||
### 2.2 获取接入凭证
|
||||
在应用详情页面,您可以找到:
|
||||
* **AppId**:应用的唯一识别 ID。
|
||||
* **SecretKey**:签名密钥。**请务必妥善保管,切勿泄露。** 在 SDK 初始化时使用此密钥可开启“签名鉴权”,防止解析接口被他人盗刷。
|
||||
|
||||
---
|
||||
|
||||
## 3. 域名与记录配置
|
||||
|
||||
### 3.1 添加域名
|
||||
1. 进入应用详情页,切换至 **域名管理** 标签。
|
||||
2. 点击 **添加域名**,输入您的业务域名(如 `api.example.com`)。
|
||||
|
||||
### 3.2 自定义解析规则
|
||||
**作用**:自定义解析规则允许您根据终端用户的网络环境(如运营商)或物理位置,为其分配最优的访问地址。通过精细化的线路调度,可以有效降低跨境或跨网访问带来的延迟,提升 App 的响应速度。
|
||||
|
||||
点击域名后的 **解析规则**,进入详细设置:
|
||||
* **解析类型**:支持 **A 记录 (IPv4)** 和 **AAAA 记录 (IPv6)**。
|
||||
* **线路选择**:可选择针对特定运营商(如:移动、电信、联通)或特定地域(如:浙江省、海外)进行精准匹配。若不选择则代表全局默认配置。
|
||||
* **解析结果**:填写您的服务器目标 IP 地址。
|
||||
* **TTL**:解析结果在客户端缓存的时间。默认为 30 秒,建议保持默认以兼顾调度灵活性。
|
||||
|
||||
---
|
||||
|
||||
## 4. 配合 CDN 实现网络加速与安全
|
||||
|
||||
Edge HTTPDNS 不仅仅提供域名解析功能,更通过与 CDN 节点的深度集成,解决了移动端常见的 **HTTPS SNI 隐匿访问**及 **跨运营商加速**问题。
|
||||
|
||||
### 4.1 自动获取 CDN 边缘节点
|
||||
如果您在系统内开通了 **CDN 加速服务**,只需将业务域名配置在 CDN 平台中,并将其 CNAME 解析指向 CDN 提供的地址:
|
||||
* **智能调度**:HTTPDNS 会自动识别该域名已接入 CDN,并针对终端用户的地理位置和运营商,智能返回最优的 **CDN 边缘节点 IP**。
|
||||
* **无感知兜底**:如果域名未接入 CDN 或未配置解析规则,HTTPDNS 将自动回源查询 **权威 DNS**,并返回业务真实的源站 IP,确保解析永不中断。
|
||||
|
||||
### 4.2 解决 HTTPS SNI 隐匿问题
|
||||
在使用 IP 直连(如 `https://1.2.3.4/path`)访问 HTTPS 业务时,传统的网络库会因为无法获取正确的 Host 导致 SSL 握手失败。
|
||||
|
||||
**我们的方案:**
|
||||
* **配合 CDN 节点**:我们的 CDN 节点已针对 HTTPDNS 进行了特殊适配。
|
||||
* **SDK 自动适配**:SDK 内部集成了标准适配器。在您的代码中,只需配合解析出的 IP 设置 HTTP 请求头的 `Host` 字段,即可透明地完成 SNI 握手,无需复杂的 SSL 改写逻辑。
|
||||
* **稳定性保障**:通过 CDN 节点的全局负载均衡,即使某个节点异常,HTTPDNS 也会实时踢除并将流量导向其他可用节点,确保业务高可用。
|
||||
|
||||
---
|
||||
|
||||
## 4. 调试与验证
|
||||
|
||||
### 4.1 在线解析测试
|
||||
在左侧菜单进入 **解析测试**:
|
||||
1. 选择您创建的 **应用**、**HTTPDNS 服务域名** 和 **待解析域名**。
|
||||
2. **模拟客户端 IP**(可选):输入特定地区的 IP,验证该地区的解析结果是否符合预期(地域调度验证)。
|
||||
3. 点击 **在线解析**,查看返回的具体 IP 列表。
|
||||
|
||||
### 4.2 访问日志查询
|
||||
在 **访问日志** 中,您可以实时监控解析请求:
|
||||
* 查看各个 AppId 下域名的解析成功率。
|
||||
* 查看请求的来源 IP、耗时以及命中的路由规则。
|
||||
|
||||
---
|
||||
|
||||
## 5. 获取 SDK
|
||||
在 **应用详情 -> SDK下载** 中:
|
||||
* 您可以下载最新版本的 Android、iOS 或 Flutter SDK 压缩包。
|
||||
* 查看配套的 **SDK 集成文档**。
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见问题 (FAQ)
|
||||
|
||||
* **Q:为什么我设置了记录,解析测试却返回为空?**
|
||||
* A:请检查记录是否已启用;或者检查该域名是否已被添加到对应的 AppId 允许列表下。
|
||||
* **Q:如何应对冷启动时的解析延迟?**
|
||||
* A:建议在 SDK 初始化后调用“解析预热”接口,提前将热点域名加载至缓存。
|
||||
* **Q:SecretKey 泄露了怎么办?**
|
||||
* A:请在应用设置中重置 SecretKey,并在 App 代码中同步更新。
|
||||
@@ -1,82 +0,0 @@
|
||||
# Edge HTTPDNS 管理员配置手册
|
||||
|
||||
本文档汇总了 Edge HTTPDNS 的核心配置流程,重点介绍了集群管理、节点在线安装以及多集群调度的详细操作。
|
||||
|
||||
---
|
||||
|
||||
## 1. 集群管理
|
||||
|
||||
集群是 HTTPDNS 服务的基本组织单元。通过设置“默认”角色,可以实现应用配置的自动关联与 SDK 侧的高可用容灾。
|
||||
|
||||
### 1.1 默认集群角色定义
|
||||
在“集群设置”中,您可以将集群开启为 **“设为默认集群”**,并指派以下角色:
|
||||
|
||||
* **默认主集群**:
|
||||
* **自动关联**:当在控制台“添加应用”时,系统会自动将该应用关联到此默认主集群。
|
||||
* **服务首选**:SDK 初始化后,会优先使用该集群的服务域名进行解析请求。
|
||||
* **默认备用集群**:
|
||||
* **自动容灾**:当默认主集群的节点全部宕机或网络不可达时,SDK 会自动切换至默认备用集群进行解析,确保业务不中断。
|
||||
* **自动关联**:与主集群一样,新应用创建时也会自动关联此备用集群信息。
|
||||
|
||||
> **注意**:同一时刻,系统内仅允许存在一个“默认主集群”和一个“默认备用集群”。新设置的默认集群会自动取消之前的旧设置。
|
||||
|
||||
### 1.2 核心参数配置
|
||||
* **服务域名**:该集群对外提供 HTTPDNS 服务的接入地址。
|
||||
* **降级超时容忍度**:指节点回源查询上游 DNS 时的最大等待时间(单位:毫秒)。若超过此阈值未获得结果,将视作解析失败。该选项可在“集群设置”中统一调整。
|
||||
* **默认 TTL**:解析结果在客户端缓存的缺省时长(默认 30s)。
|
||||
* **TLS 设置**:
|
||||
* **端口绑定**:通常绑定 `443`。
|
||||
* **SSL 证书**:必须配置合法证书,否则 SDK 的 HTTPS 请求将失败。
|
||||
* **TLS 版本**:默认支持 TLS 1.1 及以上版本。
|
||||
|
||||
---
|
||||
|
||||
## 2. 节点安装与维护
|
||||
|
||||
节点是处理解析请求的实体。Edge HTTPDNS 支持“在线安装”。
|
||||
|
||||
### 2.1 在线安装
|
||||
1. **创建节点**:在集群下点击“创建节点”,填写名称及公网 IP。
|
||||
2. **配置 SSH**:在节点详情中点击“设置 SSH”,输入服务器的 Host、Port 及登录授权。
|
||||
3. **启动安装**:在“安装信息”页面点击 **[开始安装]**。
|
||||
* 自动下发二进制文件及服务脚本。
|
||||
* 自动生成 `configs/api_httpdns.yaml`(包含节点识别需要的 `nodeId` 和 `secret`)。
|
||||
4. **实时状态**:安装成功后,节点状态变为 **[在线]**,控制台每 30 秒更新一次节点的 CPU、内存及负载数据。
|
||||
|
||||
---
|
||||
|
||||
## 3. 应用与解析规则
|
||||
|
||||
### 3.1 接入应用管理
|
||||
* **AppId/SecretKey**:创建应用后生成的凭证。应用在创建时已根据上述“默认集群”设定自动关联了服务入口。
|
||||
* **鉴权配置**:开启“签名鉴权”可配合 SDK 的 `setSecretKey` 接口,杜绝接口被盗刷风险。
|
||||
|
||||
### 3.2 智能解析策略
|
||||
* **线路/地域匹配**:根据来源运营商和地理位置返回最优 IP。
|
||||
|
||||
---
|
||||
|
||||
## 4. 调试与监控
|
||||
|
||||
### 4.1 解析测试
|
||||
在 **调试工具 -> 解析测试** 中,您可以模拟客户端请求:
|
||||
* 支持指定 **目标应用** 与 **所属集群**。
|
||||
* 支持模拟 **客户端 IP** 以验证 ECS 掩码及地域调度效果。
|
||||
* 实时展示请求 URL、客户端归属地、命中线路以及解析结果(IP 列表与 TTL)。
|
||||
|
||||
### 4.2 访问日志
|
||||
在 **访问日志** 菜单中,可实时查阅所有终端发起的解析请求:
|
||||
* 记录包括:请求时间、客户端 IP/操作系统、SDK 版本、解析域名、耗时以及最终返回的 IP 结果。
|
||||
* 支持按 AppID、域名、状态(成功/失败)或关键字搜索排查。
|
||||
|
||||
### 4.3 运行日志
|
||||
记录服务端及节点的底层运行事件:
|
||||
* 包括节点心跳、SSL 证书加载、API 连接状态等系统级信息。
|
||||
* 分为 Error、Warning、Info 等级别,是排查节点离线或连接故障的首要工具。
|
||||
|
||||
---
|
||||
|
||||
## 5. 常见问题
|
||||
|
||||
* **节点与 API 时间偏离**:若节点时间与 API Server 相差超过 30s,会导致鉴权失败(SIGN_INVALID)。请务必开启 NTP 时间同步。
|
||||
* **SDK 无法连接备用集群**:请检查默认备用集群的 SSL 证书是否有效,以及防火墙端口是否开放。
|
||||
@@ -1,92 +0,0 @@
|
||||
# SNI隐匿开发计划(HTTPDNS专项)
|
||||
|
||||
## 0. 目标
|
||||
在明文网络层(DNS 与 TLS ClientHello)中不出现真实业务域名;真实业务域名仅出现在加密后的 HTTP 层(Host / :authority)中传输。
|
||||
|
||||
## 1. 固定原则
|
||||
1. 只做 SNI 隐匿能力,不引入其他无关能力描述。
|
||||
2. 客户端不走系统 DNS 解析业务域名。
|
||||
3. TLS 握手阶段不发送真实业务域名 SNI。
|
||||
4. CDN/WAF 节点必须支持“空SNI接入 + Host路由”。
|
||||
5. 真实域名只在 HTTPS 加密通道内携带。
|
||||
|
||||
## 2. 端到端链路(目标形态)
|
||||
1. App 调用 SDK,请求解析业务域名。
|
||||
2. SDK 从 HTTPDNS 获取业务域名对应的“接入 IP 列表”(不是业务域名 DNS)。
|
||||
3. SDK 直连接入 IP 发起 TLS:
|
||||
- SNI 置空(或不发送)
|
||||
- 不出现真实业务域名
|
||||
4. TLS 建立后发起 HTTPS 请求:
|
||||
- Host / :authority = 真实业务域名
|
||||
5. CDN/WAF 节点在解密后读取 Host,将流量路由到对应业务源站。
|
||||
|
||||
## 3. SDK 改造要求
|
||||
### 3.1 连接行为
|
||||
1. 提供“按 IP 直连”的请求通道。
|
||||
2. TLS 握手时固定空 SNI,不允许带真实域名。
|
||||
3. HTTP 层强制写入真实 Host / :authority。
|
||||
|
||||
### 3.2 证书校验
|
||||
1. 仍必须做证书链校验(不允许关闭 TLS 安全校验)。
|
||||
2. 证书校验目标为接入层证书(CDN/WAF 对外证书),而非业务源站证书。
|
||||
|
||||
### 3.3 多IP与容错
|
||||
1. HTTPDNS 返回多个接入 IP 时,SDK 按顺序/策略重试。
|
||||
2. 连接失败可切换下一 IP。
|
||||
3. 缓存与过期策略保持稳定,避免频繁抖动。
|
||||
|
||||
### 3.4 多端一致性
|
||||
1. Android/iOS/Flutter 需保证一致行为:
|
||||
- 空 SNI
|
||||
- Host 注入
|
||||
- 失败重试策略
|
||||
2. 文档与示例代码同步更新。
|
||||
|
||||
## 4. CDN/WAF 节点改造要求
|
||||
### 4.1 TLS 接入
|
||||
1. 支持无 SNI ClientHello 的 TLS 握手。
|
||||
2. 为接入域名部署有效证书(覆盖客户端连接目标)。
|
||||
|
||||
### 4.2 路由逻辑
|
||||
1. 以 Host / :authority 作为业务路由主键。
|
||||
2. 路由匹配前做标准化:
|
||||
- 小写化
|
||||
- 去端口
|
||||
3. Host 未命中时返回明确错误(4xx),禁止兜底到默认站点。
|
||||
|
||||
### 4.3 回源行为
|
||||
1. 节点到源站可继续使用 HTTPS 回源。
|
||||
2. 回源主机名与证书校验按现有网关策略执行。
|
||||
|
||||
### 4.4 可观测性
|
||||
1. 增加日志字段:
|
||||
- `tlsSniPresent`(是否携带 SNI)
|
||||
- `host`
|
||||
- `routeResult`
|
||||
2. 可按“空SNI请求占比、Host路由命中率”监控。
|
||||
|
||||
## 5. 控制面(管理端)要求
|
||||
1. 页面仅展示“已启用 SNI 隐匿(空SNI)”,不提供策略切换。
|
||||
2. 集群侧需可检查“节点是否支持空SNI接入”。
|
||||
3. 发布配置时支持灰度与回滚。
|
||||
|
||||
## 6. 验收标准
|
||||
1. 抓包验证:
|
||||
- DNS 明文流量中不出现真实业务域名
|
||||
- TLS ClientHello 中不出现真实业务域名 SNI
|
||||
2. 请求验证:
|
||||
- HTTPS 请求 Host 为真实业务域名
|
||||
- CDN/WAF 按 Host 正确路由
|
||||
3. 稳定性验证:
|
||||
- 多 IP 切换成功
|
||||
- 节点故障时请求可恢复
|
||||
|
||||
## 7. 上线顺序
|
||||
1. 先升级 CDN/WAF 节点能力(空SNI接入 + Host路由)。
|
||||
2. 再升级 SDK(空SNI + Host注入)。
|
||||
3. 最后按应用灰度开启,观察指标后全量。
|
||||
|
||||
## 8. 风险与约束
|
||||
1. 若 CDN/WAF 不支持空 SNI,链路会在握手阶段失败。
|
||||
2. 若 Host 路由不严格,可能出现串站风险。
|
||||
3. 若客户端错误关闭证书校验,会引入严重安全风险。
|
||||
@@ -1,7 +1,7 @@
|
||||
package teaconst
|
||||
|
||||
const (
|
||||
Version = "1.4.8"
|
||||
Version = "1.4.9"
|
||||
|
||||
ProductName = "Edge HTTPDNS"
|
||||
ProcessName = "edge-httpdns"
|
||||
|
||||
@@ -157,8 +157,8 @@ func NewResolveServer(quitCh <-chan struct{}, snapshotManager *SnapshotManager)
|
||||
instance.handler = mux
|
||||
|
||||
instance.tlsConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS11,
|
||||
NextProtos: []string{"http/1.1"},
|
||||
MinVersion: tls.VersionTLS11,
|
||||
NextProtos: []string{"http/1.1"},
|
||||
GetCertificate: instance.getCertificate,
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ func (s *ResolveServer) getCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate
|
||||
|
||||
type snapshotTLSConfig struct {
|
||||
Listen []*serverconfigs.NetworkAddressConfig `json:"listen"`
|
||||
SSLPolicy *sslconfigs.SSLPolicy `json:"sslPolicy"`
|
||||
SSLPolicy *sslconfigs.SSLPolicy `json:"sslPolicy"`
|
||||
}
|
||||
|
||||
func (s *ResolveServer) parseTLSConfig(snapshot *LoadedSnapshot) *snapshotTLSConfig {
|
||||
@@ -270,23 +270,24 @@ func (s *ResolveServer) desiredAddrs(snapshot *LoadedSnapshot) []string {
|
||||
func (s *ResolveServer) reloadCertFromSnapshot(snapshot *LoadedSnapshot) {
|
||||
cfg := s.parseTLSConfig(snapshot)
|
||||
if cfg == nil || cfg.SSLPolicy == nil || len(cfg.SSLPolicy.Certs) == 0 {
|
||||
// 没有TLS配置,标记已处理(不需要重试)
|
||||
s.certMu.Lock()
|
||||
s.certSnapshotAt = snapshot.LoadedAt
|
||||
s.certMu.Unlock()
|
||||
reportRuntimeLog("info", "tls", "resolve", "no TLS policy in cluster snapshot, skipped cert reload", fmt.Sprintf("cert-skip-%d", snapshot.LoadedAt))
|
||||
return
|
||||
}
|
||||
if err := cfg.SSLPolicy.Init(context.Background()); err != nil {
|
||||
log.Println("[HTTPDNS_NODE][resolve]init SSLPolicy failed:", err.Error())
|
||||
s.certMu.Lock()
|
||||
s.certSnapshotAt = snapshot.LoadedAt
|
||||
s.certMu.Unlock()
|
||||
reportRuntimeLog("error", "tls", "resolve", "init SSLPolicy failed: "+err.Error(), fmt.Sprintf("cert-err-%d", snapshot.LoadedAt))
|
||||
// 不更新 certSnapshotAt,下次 watchLoop 会重试
|
||||
return
|
||||
}
|
||||
cert := cfg.SSLPolicy.FirstCert()
|
||||
if cert == nil {
|
||||
s.certMu.Lock()
|
||||
s.certSnapshotAt = snapshot.LoadedAt
|
||||
s.certMu.Unlock()
|
||||
log.Println("[HTTPDNS_NODE][resolve]SSLPolicy has no valid certificate after Init")
|
||||
reportRuntimeLog("error", "tls", "resolve", "SSLPolicy has no valid certificate after Init", fmt.Sprintf("cert-err-%d", snapshot.LoadedAt))
|
||||
// 不更新 certSnapshotAt,下次 watchLoop 会重试
|
||||
return
|
||||
}
|
||||
|
||||
@@ -295,6 +296,7 @@ func (s *ResolveServer) reloadCertFromSnapshot(snapshot *LoadedSnapshot) {
|
||||
s.certSnapshotAt = snapshot.LoadedAt
|
||||
s.certMu.Unlock()
|
||||
log.Println("[HTTPDNS_NODE][resolve]TLS certificate reloaded from snapshot")
|
||||
reportRuntimeLog("info", "tls", "resolve", "TLS certificate reloaded from snapshot successfully", fmt.Sprintf("cert-ok-%d", snapshot.LoadedAt))
|
||||
}
|
||||
|
||||
func (s *ResolveServer) startListener(addr string) error {
|
||||
@@ -561,27 +563,29 @@ func (s *ResolveServer) handleResolve(writer http.ResponseWriter, request *http.
|
||||
},
|
||||
})
|
||||
|
||||
s.enqueueAccessLog(&pb.HTTPDNSAccessLog{
|
||||
RequestId: requestID,
|
||||
ClusterId: snapshot.ClusterID,
|
||||
NodeId: snapshot.NodeID,
|
||||
AppId: loadedApp.App.GetAppId(),
|
||||
AppName: loadedApp.App.GetName(),
|
||||
Domain: domain,
|
||||
Qtype: qtype,
|
||||
ClientIP: clientProfile.IP,
|
||||
ClientRegion: clientProfile.RegionText,
|
||||
Carrier: clientProfile.Carrier,
|
||||
SdkVersion: strings.TrimSpace(query.Get("sdk_version")),
|
||||
Os: strings.TrimSpace(query.Get("os")),
|
||||
ResultIPs: strings.Join(resultIPs, ","),
|
||||
Status: "success",
|
||||
ErrorCode: "none",
|
||||
CostMs: int32(time.Since(startAt).Milliseconds()),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
Day: time.Now().Format("20060102"),
|
||||
Summary: summary,
|
||||
})
|
||||
if s.isAccessLogEnabled(snapshot) {
|
||||
s.enqueueAccessLog(&pb.HTTPDNSAccessLog{
|
||||
RequestId: requestID,
|
||||
ClusterId: snapshot.ClusterID,
|
||||
NodeId: snapshot.NodeID,
|
||||
AppId: loadedApp.App.GetAppId(),
|
||||
AppName: loadedApp.App.GetName(),
|
||||
Domain: domain,
|
||||
Qtype: qtype,
|
||||
ClientIP: clientProfile.IP,
|
||||
ClientRegion: clientProfile.RegionText,
|
||||
Carrier: clientProfile.Carrier,
|
||||
SdkVersion: strings.TrimSpace(query.Get("sdk_version")),
|
||||
Os: strings.TrimSpace(query.Get("os")),
|
||||
ResultIPs: strings.Join(resultIPs, ","),
|
||||
Status: "success",
|
||||
ErrorCode: "none",
|
||||
CostMs: int32(time.Since(startAt).Milliseconds()),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
Day: time.Now().Format("20060102"),
|
||||
Summary: summary,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func pickDefaultTTL(snapshot *LoadedSnapshot, app *pb.HTTPDNSApp) int32 {
|
||||
@@ -655,27 +659,29 @@ func (s *ResolveServer) writeFailedResolve(
|
||||
nodeID = snapshot.NodeID
|
||||
}
|
||||
|
||||
s.enqueueAccessLog(&pb.HTTPDNSAccessLog{
|
||||
RequestId: requestID,
|
||||
ClusterId: clusterID,
|
||||
NodeId: nodeID,
|
||||
AppId: appID,
|
||||
AppName: appName,
|
||||
Domain: domain,
|
||||
Qtype: qtype,
|
||||
ClientIP: clientProfile.IP,
|
||||
ClientRegion: clientProfile.RegionText,
|
||||
Carrier: clientProfile.Carrier,
|
||||
SdkVersion: strings.TrimSpace(query.Get("sdk_version")),
|
||||
Os: strings.TrimSpace(query.Get("os")),
|
||||
ResultIPs: "",
|
||||
Status: "failed",
|
||||
ErrorCode: errorCode,
|
||||
CostMs: int32(time.Since(startAt).Milliseconds()),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
Day: time.Now().Format("20060102"),
|
||||
Summary: summary,
|
||||
})
|
||||
if s.isAccessLogEnabled(snapshot) {
|
||||
s.enqueueAccessLog(&pb.HTTPDNSAccessLog{
|
||||
RequestId: requestID,
|
||||
ClusterId: clusterID,
|
||||
NodeId: nodeID,
|
||||
AppId: appID,
|
||||
AppName: appName,
|
||||
Domain: domain,
|
||||
Qtype: qtype,
|
||||
ClientIP: clientProfile.IP,
|
||||
ClientRegion: clientProfile.RegionText,
|
||||
Carrier: clientProfile.Carrier,
|
||||
SdkVersion: strings.TrimSpace(query.Get("sdk_version")),
|
||||
Os: strings.TrimSpace(query.Get("os")),
|
||||
ResultIPs: "",
|
||||
Status: "failed",
|
||||
ErrorCode: errorCode,
|
||||
CostMs: int32(time.Since(startAt).Milliseconds()),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
Day: time.Now().Format("20060102"),
|
||||
Summary: summary,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ResolveServer) writeResolveJSON(writer http.ResponseWriter, status int, resp *resolveResponse) {
|
||||
@@ -1424,6 +1430,17 @@ func ruleRegionSummary(rule *pb.HTTPDNSCustomRule) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *ResolveServer) isAccessLogEnabled(snapshot *LoadedSnapshot) bool {
|
||||
if snapshot == nil || snapshot.ClusterID <= 0 {
|
||||
return true
|
||||
}
|
||||
cluster := snapshot.Clusters[snapshot.ClusterID]
|
||||
if cluster == nil {
|
||||
return true
|
||||
}
|
||||
return cluster.GetAccessLogIsOn()
|
||||
}
|
||||
|
||||
func (s *ResolveServer) enqueueAccessLog(item *pb.HTTPDNSAccessLog) {
|
||||
if item == nil {
|
||||
return
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user