Compare commits

41 Commits
v1.4.7 ... main

Author SHA1 Message Date
robin
17e182b413 v1.5.1 增强程序稳定性 2026-03-22 17:37:40 +08:00
robin
afbaaa869c 1.5.0 2026-03-13 14:25:13 +08:00
robin
a25a474d6a 删除多余文件 2026-03-05 20:37:13 +08:00
robin
fb8348ef47 merge 2026-03-05 19:45:50 +08:00
robin
491ade1bc3 sdk final 2026-03-05 16:59:19 +08:00
robin
a10f3f3740 ios demo 2026-03-05 02:52:45 +08:00
robin
49776c3d0a ios test 2026-03-05 02:44:43 +08:00
robin
49021c7415 Merge branch 'feat-httpdns-sdk' 2026-03-04 21:37:30 +08:00
robin
c040d7c90d SDK下载乱码问题修复 2026-03-04 19:44:21 +08:00
robin
30a1b4fa5e 文件恢复 2026-03-04 19:14:57 +08:00
robin
54e808a85d 编码修复 2026-03-04 18:41:01 +08:00
robin
c6cbf3c505 chore: remove .claude from repository 2026-03-04 18:07:36 +08:00
robin
532891fad0 feat: sync httpdns sdk/platform updates without large binaries 2026-03-04 17:59:14 +08:00
robin
853897a6f8 节点自动升级功能之前的版本 2026-03-02 23:42:55 +08:00
robin
2a76d1773d 换成单集群模式 2026-03-02 20:07:53 +08:00
robin
5d0b7c7e91 带阿里标识的版本 2026-02-28 18:55:33 +08:00
robin
150799f41d 管理端全部功能跑通 2026-02-27 10:35:22 +08:00
robin
4d275c921d 前端页面 2026-02-24 22:43:49 +08:00
robin
2eb32b9f1f 前端页面 2026-02-24 19:10:27 +08:00
robin
60dc87e0f2 前端页面 2026-02-24 11:33:44 +08:00
Robin
f3af234308 阿里sdk 2026-02-20 17:56:24 +08:00
robin
9d941f8bb9 文件补充 2026-02-20 12:30:55 +08:00
robin
9378cdc8f0 补充文件 2026-02-19 22:40:33 +08:00
robin
6d6462c65a 菜单样式更新 2026-02-16 10:32:35 +08:00
robin
7d83a9f7f4 UI update 2026-02-15 19:45:30 +08:00
robin
92ffa1ab77 登录页面改造 2026-02-15 15:16:20 +08:00
robin
2a811183c4 登录页面改造 2026-02-15 14:27:28 +08:00
robin
afbc0cde7e 登录页面改造 2026-02-15 02:11:49 +08:00
robin
468d0eeffc 登录页面改造 2026-02-15 01:56:39 +08:00
robin
dd396d31b5 样式修改 2026-02-14 23:23:35 +08:00
robin
eafac7a204 文件清理 2026-02-14 17:28:12 +08:00
robin
4259026c6e 文件清理 2026-02-14 17:12:38 +08:00
robin
f38505e210 chore: sync local changes 2026-02-14 01:43:38 +08:00
robin
e9093baffb 引入lumberjack和fluentbit自动分发 2026-02-13 22:36:17 +08:00
robin
c6da67db79 lumberjack改造前 2026-02-12 21:37:55 +08:00
robin
c28317ee07 dns clickhouse改造 2026-02-10 23:43:05 +08:00
robin
1bb8140a41 dns clickhouse改造 2026-02-10 19:30:44 +08:00
robin
4812ad5aaf 错误日志查询问题修复 2026-02-08 22:44:12 +08:00
robin
b7388d83b0 日常查询由mysql改为clickhouse 2026-02-08 02:00:51 +08:00
robin
bc223fd1aa 主分支代码 2026-02-07 20:30:31 +08:00
unknown
3b042d1dad 1.4.5.2 2026-02-04 20:27:13 +08:00
1658 changed files with 124384 additions and 36082 deletions

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
*.sh text eol=lf
*.bash text eol=lf
*.zsh text eol=lf

17
.gitignore vendored
View File

@@ -5,3 +5,20 @@ deploy/fluent-bit/logs.db
deploy/fluent-bit/logs.db-shm
deploy/fluent-bit/logs.db-wal
deploy/fluent-bit/storage/
/pkg/
/.claude/
CLAUDE.md
CHANGELOG-1.5.1.md
docs/*
# Test artifacts
EdgeDNS/agents.test.exe
EdgeAdmin/test_db2.go
# Runtime data (generated at runtime, not source)
EdgeNode/build/data/
EdgeDNS/build/data/
# Local large build artifacts
EdgeAdmin/edge-admin.exe
EdgeAPI/deploy/edge-node-linux-amd64-v*.zip

View File

@@ -41,17 +41,7 @@ function build() {
cd "$ROOT""/../../EdgeNode/build" || exit
echo "=============================="
for arch in "${NODE_ARCHITECTS[@]}"; do
# 查找 zip 文件时不包含 plus 标记
if [ "${TAG}" = "plus" ]; then
NODE_ZIP_FILE="$ROOT""/../../EdgeNode/dist/edge-node-linux-${arch}-v${NodeVersion}.zip"
else
NODE_ZIP_FILE="$ROOT""/../../EdgeNode/dist/edge-node-linux-${arch}-${TAG}-v${NodeVersion}.zip"
fi
if [ ! -f "$NODE_ZIP_FILE" ]; then
./build.sh linux "$arch" $TAG
else
echo "use built node linux/$arch/v${NodeVersion}"
fi
./build.sh linux "$arch" $TAG
done
echo "=============================="
cd - || exit
@@ -94,6 +84,32 @@ function build() {
fi
fi
# build edge-httpdns
HTTPDNS_ROOT=$ROOT"/../../EdgeHttpDNS"
if [ -d "$HTTPDNS_ROOT" ]; then
HTTPDNSNodeVersion=$(lookup-version "$ROOT""/../../EdgeHttpDNS/internal/const/const.go")
echo "building edge-httpdns v${HTTPDNSNodeVersion} ..."
EDGE_HTTPDNS_NODE_BUILD_SCRIPT=$ROOT"/../../EdgeHttpDNS/build/build.sh"
if [ ! -f "$EDGE_HTTPDNS_NODE_BUILD_SCRIPT" ]; then
echo "unable to find edge-httpdns build script 'EdgeHttpDNS/build/build.sh'"
exit
fi
cd "$ROOT""/../../EdgeHttpDNS/build" || exit
echo "=============================="
architects=("amd64")
#architects=("amd64" "arm64")
for arch in "${architects[@]}"; do
# always rebuild to avoid reusing stale zip when version number is unchanged
./build.sh linux "$arch"
done
echo "=============================="
cd - || exit
for arch in "${architects[@]}"; do
cp "$ROOT""/../../EdgeHttpDNS/dist/edge-httpdns-linux-${arch}-v${HTTPDNSNodeVersion}.zip" "$ROOT"/deploy/edge-httpdns-linux-"${arch}"-v"${HTTPDNSNodeVersion}".zip
done
fi
# build sql
if [ $TAG = "plus" ]; then
echo "building sql ..."
@@ -102,13 +118,11 @@ function build() {
# copy files
echo "copying ..."
if [ ! -d "$DIST" ]; then
mkdir "$DIST"
mkdir "$DIST"/bin
mkdir "$DIST"/configs
mkdir "$DIST"/logs
mkdir "$DIST"/data
fi
rm -rf "$DIST"
mkdir -p "$DIST"/bin
mkdir -p "$DIST"/configs
mkdir -p "$DIST"/logs
mkdir -p "$DIST"/data
cp "$ROOT"/configs/api.template.yaml "$DIST"/configs/
cp "$ROOT"/configs/db.template.yaml "$DIST"/configs/
# 复制 EdgeCommon 的配置文件(如果存在)
@@ -122,42 +136,6 @@ function build() {
rm -f "$DIST"/deploy/.gitignore
cp -R "$ROOT"/installers "$DIST"/
# copy fluent-bit templates and local packages from repo root
FLUENT_ROOT="$ROOT/../../deploy/fluent-bit"
FLUENT_DIST="$DIST/deploy/fluent-bit"
if [ -d "$FLUENT_ROOT" ]; then
rm -rf "$FLUENT_DIST"
mkdir -p "$FLUENT_DIST"
FLUENT_FILES=(
"fluent-bit.conf"
"fluent-bit-dns.conf"
"fluent-bit-https.conf"
"fluent-bit-dns-https.conf"
"fluent-bit-windows.conf"
"fluent-bit-windows-https.conf"
"parsers.conf"
"clickhouse-upstream.conf"
"clickhouse-upstream-windows.conf"
"logrotate.conf"
"README.md"
)
for file in "${FLUENT_FILES[@]}"; do
if [ -f "$FLUENT_ROOT/$file" ]; then
cp "$FLUENT_ROOT/$file" "$FLUENT_DIST/"
fi
done
if [ -d "$FLUENT_ROOT/packages" ]; then
cp -R "$FLUENT_ROOT/packages" "$FLUENT_DIST/"
fi
# remove local runtime artifacts if present
rm -f "$FLUENT_DIST/.gitignore"
rm -f "$FLUENT_DIST"/logs.db*
rm -rf "$FLUENT_DIST/storage"
fi
# building edge installer
echo "building node installer ..."
architects=("amd64")

View File

@@ -1,5 +1,5 @@
user: root
password: 123456
host: 127.0.0.1:3306
host: 127.0.0.1:3308
database: db_edge
boolFields: [ "uamIsOn", "followPort", "requestHostExcludingPort", "autoRemoteStart", "autoInstallNftables", "enableIPLists", "detectAgents", "checkingPorts", "enableRecordHealthCheck", "offlineIsNotified", "http2Enabled", "http3Enabled", "enableHTTP2", "retry50X", "retry40X", "autoSystemTuning", "disableDefaultDB", "autoTrimDisks", "enableGlobalPages", "ignoreLocal", "ignoreSearchEngine" ]

View File

@@ -3,5 +3,11 @@
# 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 [ "$1" = "--force" ] || [ ! -f "$SQL_JSON" ]; then
rm -f "$SQL_JSON"
go run "${CWD}"/../cmd/sql-dump/main.go -dir="${CWD}"
else
echo "sql.json exists, skip (use --force to regenerate)"
fi

View File

@@ -12,6 +12,8 @@ import (
)
func main() {
Tea.Env = "prod"
db, err := dbs.Default()
if err != nil {
fmt.Println("[ERROR]" + err.Error())

View File

@@ -1,6 +1,7 @@
package clickhouse
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
@@ -12,13 +13,11 @@ import (
"time"
)
// Client 通过 HTTP 接口执行只读查询SELECT返回 JSONEachRow 解析为 map 或结构体
type Client struct {
cfg *Config
httpCli *http.Client
}
// NewClient 使用共享配置创建客户端
func NewClient() *Client {
cfg := SharedConfig()
transport := &http.Transport{}
@@ -28,6 +27,7 @@ func NewClient() *Client {
ServerName: cfg.TLSServerName,
}
}
return &Client{
cfg: cfg,
httpCli: &http.Client{
@@ -37,21 +37,20 @@ func NewClient() *Client {
}
}
// IsConfigured 是否已配置
func (c *Client) IsConfigured() bool {
return c.cfg != nil && c.cfg.IsConfigured()
}
// Query 执行 SELECT将每行 JSON 解析到 dest 切片dest 元素类型需为 *struct 或 map
func (c *Client) Query(ctx context.Context, query string, dest interface{}) error {
if !c.IsConfigured() {
return fmt.Errorf("clickhouse: not configured")
}
// 强制 JSONEachRow 便于解析
q := strings.TrimSpace(query)
if !strings.HasSuffix(strings.ToUpper(q), "FORMAT JSONEACHROW") {
query = q + " FORMAT JSONEachRow"
}
u := c.buildURL(query)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
@@ -60,28 +59,32 @@ func (c *Client) Query(ctx context.Context, query string, dest interface{}) erro
if c.cfg.User != "" || c.cfg.Password != "" {
req.SetBasicAuth(c.cfg.User, c.cfg.Password)
}
resp, err := c.httpCli.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("clickhouse HTTP %d: %s", resp.StatusCode, string(body))
}
dec := json.NewDecoder(resp.Body)
return decodeRows(dec, dest)
}
// QueryRow 执行仅返回一行的查询,将结果解析到 dest*struct 或 *map
func (c *Client) QueryRow(ctx context.Context, query string, dest interface{}) error {
if !c.IsConfigured() {
return fmt.Errorf("clickhouse: not configured")
}
q := strings.TrimSpace(query)
if !strings.HasSuffix(strings.ToUpper(q), "FORMAT JSONEACHROW") {
query = q + " FORMAT JSONEachRow"
}
u := c.buildURL(query)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
@@ -90,32 +93,109 @@ func (c *Client) QueryRow(ctx context.Context, query string, dest interface{}) e
if c.cfg.User != "" || c.cfg.Password != "" {
req.SetBasicAuth(c.cfg.User, c.cfg.Password)
}
resp, err := c.httpCli.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("clickhouse HTTP %d: %s", resp.StatusCode, string(body))
}
dec := json.NewDecoder(resp.Body)
return decodeOneRow(dec, dest)
}
func (c *Client) Execute(ctx context.Context, query string) error {
if !c.IsConfigured() {
return fmt.Errorf("clickhouse: not configured")
}
u := c.buildURL(strings.TrimSpace(query))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, nil)
if err != nil {
return err
}
if c.cfg.User != "" || c.cfg.Password != "" {
req.SetBasicAuth(c.cfg.User, c.cfg.Password)
}
resp, err := c.httpCli.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("clickhouse HTTP %d: %s", resp.StatusCode, string(body))
}
return nil
}
func (c *Client) InsertJSONEachRow(ctx context.Context, insertSQL string, rows []map[string]interface{}) error {
if len(rows) == 0 {
return nil
}
if !c.IsConfigured() {
return fmt.Errorf("clickhouse: not configured")
}
query := strings.TrimSpace(insertSQL)
if !strings.HasSuffix(strings.ToUpper(query), "FORMAT JSONEACHROW") {
query += " FORMAT JSONEachRow"
}
var payload bytes.Buffer
for _, row := range rows {
if row == nil {
continue
}
data, err := json.Marshal(row)
if err != nil {
return err
}
payload.Write(data)
payload.WriteByte('\n')
}
u := c.buildURL(query)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, &payload)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if c.cfg.User != "" || c.cfg.Password != "" {
req.SetBasicAuth(c.cfg.User, c.cfg.Password)
}
resp, err := c.httpCli.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("clickhouse HTTP %d: %s", resp.StatusCode, string(body))
}
return nil
}
func (c *Client) buildURL(query string) string {
scheme := "http"
if c.cfg != nil && strings.EqualFold(c.cfg.Scheme, "https") {
scheme = "https"
}
rawURL := fmt.Sprintf("%s://%s:%d/?query=%s&database=%s",
return fmt.Sprintf("%s://%s:%d/?query=%s&database=%s",
scheme, c.cfg.Host, c.cfg.Port, url.QueryEscape(query), url.QueryEscape(c.cfg.Database))
return rawURL
}
// decodeRows 将 JSONEachRow 流解析到 slice元素类型须为 *struct 或 *[]map[string]interface{}
func decodeRows(dec *json.Decoder, dest interface{}) error {
// dest 应为 *[]*SomeStruct 或 *[]map[string]interface{}
switch d := dest.(type) {
case *[]map[string]interface{}:
*d = (*d)[:0]
@@ -130,7 +210,7 @@ func decodeRows(dec *json.Decoder, dest interface{}) error {
*d = append(*d, row)
}
default:
return fmt.Errorf("clickhouse: unsupported dest type for Query (use *[]map[string]interface{} or implement decoder)")
return fmt.Errorf("clickhouse: unsupported dest type for Query (use *[]map[string]interface{})")
}
}

View File

@@ -12,15 +12,15 @@ import (
)
const (
envHost = "CLICKHOUSE_HOST"
envPort = "CLICKHOUSE_PORT"
envUser = "CLICKHOUSE_USER"
envPassword = "CLICKHOUSE_PASSWORD"
envDatabase = "CLICKHOUSE_DATABASE"
envScheme = "CLICKHOUSE_SCHEME"
defaultPort = 8443
defaultDB = "default"
defaultScheme = "https"
envHost = "CLICKHOUSE_HOST"
envPort = "CLICKHOUSE_PORT"
envUser = "CLICKHOUSE_USER"
envPassword = "CLICKHOUSE_PASSWORD"
envDatabase = "CLICKHOUSE_DATABASE"
envScheme = "CLICKHOUSE_SCHEME"
defaultPort = 8443
defaultDB = "default"
defaultScheme = "https"
)
var (

View File

@@ -0,0 +1,470 @@
package clickhouse
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
const httpDNSAccessLogsTable = "httpdns_access_logs_ingest"
type HTTPDNSAccessLogRow struct {
RequestId string
ClusterId int64
NodeId int64
AppId string
AppName string
Domain string
QType string
ClientIP string
ClientRegion string
Carrier string
SDKVersion string
OS string
ResultIPs string
Status string
ErrorCode string
CostMs int32
CreatedAt int64
Day string
Summary string
}
type HTTPDNSAccessLogListFilter struct {
Day string
ClusterId int64
NodeId int64
AppId string
AppIds []string
Domain string
Status string
Keyword string
Offset int64
Size int64
}
type HTTPDNSAccessLogHourlyStat struct {
Hour string
CountRequests int64
}
type HTTPDNSAccessLogDailyStat struct {
Day string
CountRequests int64
}
type HTTPDNSAccessLogTopAppStat struct {
AppId string
AppName string
CountRequests int64
}
type HTTPDNSAccessLogTopDomainStat struct {
Domain string
CountRequests int64
}
type HTTPDNSAccessLogTopNodeStat struct {
ClusterId uint32
NodeId uint32
CountRequests int64
}
type HTTPDNSAccessLogsStore struct {
client *Client
}
func NewHTTPDNSAccessLogsStore() *HTTPDNSAccessLogsStore {
return &HTTPDNSAccessLogsStore{client: NewClient()}
}
func (s *HTTPDNSAccessLogsStore) Client() *Client {
return s.client
}
func (s *HTTPDNSAccessLogsStore) Insert(ctx context.Context, logs []*pb.HTTPDNSAccessLog) error {
if len(logs) == 0 {
return nil
}
if !s.client.IsConfigured() {
return fmt.Errorf("clickhouse: not configured")
}
rows := make([]map[string]interface{}, 0, len(logs))
for _, item := range logs {
if item == nil {
continue
}
rows = append(rows, map[string]interface{}{
"request_id": item.GetRequestId(),
"cluster_id": item.GetClusterId(),
"node_id": item.GetNodeId(),
"app_id": item.GetAppId(),
"app_name": item.GetAppName(),
"domain": item.GetDomain(),
"qtype": item.GetQtype(),
"client_ip": item.GetClientIP(),
"client_region": item.GetClientRegion(),
"carrier": item.GetCarrier(),
"sdk_version": item.GetSdkVersion(),
"os": item.GetOs(),
"result_ips": item.GetResultIPs(),
"status": item.GetStatus(),
"error_code": item.GetErrorCode(),
"cost_ms": item.GetCostMs(),
"created_at": item.GetCreatedAt(),
"day": item.GetDay(),
"summary": item.GetSummary(),
})
}
query := fmt.Sprintf("INSERT INTO %s (request_id, cluster_id, node_id, app_id, app_name, domain, qtype, client_ip, client_region, carrier, sdk_version, os, result_ips, status, error_code, cost_ms, created_at, day, summary)",
s.tableName())
return s.client.InsertJSONEachRow(ctx, query, rows)
}
func (s *HTTPDNSAccessLogsStore) Count(ctx context.Context, f HTTPDNSAccessLogListFilter) (int64, error) {
if !s.client.IsConfigured() {
return 0, fmt.Errorf("clickhouse: not configured")
}
conditions := s.buildConditions(f)
query := fmt.Sprintf("SELECT count() AS count FROM %s", s.tableName())
if len(conditions) > 0 {
query += " WHERE " + strings.Join(conditions, " AND ")
}
row := map[string]interface{}{}
if err := s.client.QueryRow(ctx, query, &row); err != nil {
return 0, err
}
return toInt64(row["count"]), nil
}
func (s *HTTPDNSAccessLogsStore) List(ctx context.Context, f HTTPDNSAccessLogListFilter) ([]*HTTPDNSAccessLogRow, error) {
if !s.client.IsConfigured() {
return nil, fmt.Errorf("clickhouse: not configured")
}
size := f.Size
if size <= 0 {
size = 20
}
if size > 1000 {
size = 1000
}
offset := f.Offset
if offset < 0 {
offset = 0
}
conditions := s.buildConditions(f)
query := fmt.Sprintf("SELECT request_id, cluster_id, node_id, app_id, app_name, domain, qtype, client_ip, client_region, carrier, sdk_version, os, result_ips, status, error_code, cost_ms, created_at, day, summary FROM %s",
s.tableName())
if len(conditions) > 0 {
query += " WHERE " + strings.Join(conditions, " AND ")
}
query += " ORDER BY created_at DESC, request_id DESC"
query += fmt.Sprintf(" LIMIT %d OFFSET %d", size, offset)
rawRows := []map[string]interface{}{}
if err := s.client.Query(ctx, query, &rawRows); err != nil {
return nil, err
}
result := make([]*HTTPDNSAccessLogRow, 0, len(rawRows))
for _, row := range rawRows {
result = append(result, &HTTPDNSAccessLogRow{
RequestId: toString(row["request_id"]),
ClusterId: toInt64(row["cluster_id"]),
NodeId: toInt64(row["node_id"]),
AppId: toString(row["app_id"]),
AppName: toString(row["app_name"]),
Domain: toString(row["domain"]),
QType: toString(row["qtype"]),
ClientIP: toString(row["client_ip"]),
ClientRegion: toString(row["client_region"]),
Carrier: toString(row["carrier"]),
SDKVersion: toString(row["sdk_version"]),
OS: toString(row["os"]),
ResultIPs: toString(row["result_ips"]),
Status: toString(row["status"]),
ErrorCode: toString(row["error_code"]),
CostMs: int32(toInt64(row["cost_ms"])),
CreatedAt: toInt64(row["created_at"]),
Day: toString(row["day"]),
Summary: toString(row["summary"]),
})
}
return result, nil
}
func (s *HTTPDNSAccessLogsStore) FindHourlyStats(ctx context.Context, hourFrom string, hourTo string) ([]*HTTPDNSAccessLogHourlyStat, error) {
if !s.client.IsConfigured() {
return nil, fmt.Errorf("clickhouse: not configured")
}
query := fmt.Sprintf(
"SELECT formatDateTime(toDateTime(created_at), '%%Y%%m%%d%%H') AS hour, count() AS count_requests FROM %s WHERE formatDateTime(toDateTime(created_at), '%%Y%%m%%d%%H') BETWEEN '%s' AND '%s' GROUP BY hour ORDER BY hour ASC",
s.tableName(),
escapeString(hourFrom),
escapeString(hourTo),
)
rows := []map[string]interface{}{}
if err := s.client.Query(ctx, query, &rows); err != nil {
return nil, err
}
result := make([]*HTTPDNSAccessLogHourlyStat, 0, len(rows))
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogHourlyStat{
Hour: toString(row["hour"]),
CountRequests: toInt64(row["count_requests"]),
})
}
return result, nil
}
func (s *HTTPDNSAccessLogsStore) FindDailyStats(ctx context.Context, dayFrom string, dayTo string) ([]*HTTPDNSAccessLogDailyStat, error) {
if !s.client.IsConfigured() {
return nil, fmt.Errorf("clickhouse: not configured")
}
query := fmt.Sprintf(
"SELECT day, count() AS count_requests FROM %s WHERE day BETWEEN '%s' AND '%s' GROUP BY day ORDER BY day ASC",
s.tableName(),
escapeString(dayFrom),
escapeString(dayTo),
)
rows := []map[string]interface{}{}
if err := s.client.Query(ctx, query, &rows); err != nil {
return nil, err
}
result := make([]*HTTPDNSAccessLogDailyStat, 0, len(rows))
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogDailyStat{
Day: toString(row["day"]),
CountRequests: toInt64(row["count_requests"]),
})
}
return result, nil
}
func (s *HTTPDNSAccessLogsStore) ListTopApps(ctx context.Context, dayFrom string, dayTo string, limit int64) ([]*HTTPDNSAccessLogTopAppStat, error) {
if !s.client.IsConfigured() {
return nil, fmt.Errorf("clickhouse: not configured")
}
if limit <= 0 {
limit = 10
}
query := fmt.Sprintf(
"SELECT app_id, max(app_name) AS app_name, count() AS count_requests FROM %s WHERE day BETWEEN '%s' AND '%s' GROUP BY app_id ORDER BY count_requests DESC LIMIT %d",
s.tableName(),
escapeString(dayFrom),
escapeString(dayTo),
limit,
)
rows := []map[string]interface{}{}
if err := s.client.Query(ctx, query, &rows); err != nil {
return nil, err
}
result := make([]*HTTPDNSAccessLogTopAppStat, 0, len(rows))
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogTopAppStat{
AppId: toString(row["app_id"]),
AppName: toString(row["app_name"]),
CountRequests: toInt64(row["count_requests"]),
})
}
return result, nil
}
func (s *HTTPDNSAccessLogsStore) ListTopDomains(ctx context.Context, dayFrom string, dayTo string, limit int64) ([]*HTTPDNSAccessLogTopDomainStat, error) {
if !s.client.IsConfigured() {
return nil, fmt.Errorf("clickhouse: not configured")
}
if limit <= 0 {
limit = 10
}
query := fmt.Sprintf(
"SELECT domain, count() AS count_requests FROM %s WHERE day BETWEEN '%s' AND '%s' AND domain != '' GROUP BY domain ORDER BY count_requests DESC LIMIT %d",
s.tableName(),
escapeString(dayFrom),
escapeString(dayTo),
limit,
)
rows := []map[string]interface{}{}
if err := s.client.Query(ctx, query, &rows); err != nil {
return nil, err
}
result := make([]*HTTPDNSAccessLogTopDomainStat, 0, len(rows))
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogTopDomainStat{
Domain: toString(row["domain"]),
CountRequests: toInt64(row["count_requests"]),
})
}
return result, nil
}
func (s *HTTPDNSAccessLogsStore) ListTopNodes(ctx context.Context, dayFrom string, dayTo string, limit int64) ([]*HTTPDNSAccessLogTopNodeStat, error) {
if !s.client.IsConfigured() {
return nil, fmt.Errorf("clickhouse: not configured")
}
if limit <= 0 {
limit = 10
}
query := fmt.Sprintf(
"SELECT min(cluster_id) AS cluster_id, node_id, count() AS count_requests FROM %s WHERE day BETWEEN '%s' AND '%s' GROUP BY node_id ORDER BY count_requests DESC LIMIT %d",
s.tableName(),
escapeString(dayFrom),
escapeString(dayTo),
limit,
)
rows := []map[string]interface{}{}
if err := s.client.Query(ctx, query, &rows); err != nil {
return nil, err
}
result := make([]*HTTPDNSAccessLogTopNodeStat, 0, len(rows))
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogTopNodeStat{
ClusterId: uint32(toInt64(row["cluster_id"])),
NodeId: uint32(toInt64(row["node_id"])),
CountRequests: toInt64(row["count_requests"]),
})
}
return result, nil
}
func HTTPDNSRowToPB(row *HTTPDNSAccessLogRow) *pb.HTTPDNSAccessLog {
if row == nil {
return nil
}
return &pb.HTTPDNSAccessLog{
RequestId: row.RequestId,
ClusterId: row.ClusterId,
NodeId: row.NodeId,
AppId: row.AppId,
AppName: row.AppName,
Domain: row.Domain,
Qtype: row.QType,
ClientIP: row.ClientIP,
ClientRegion: row.ClientRegion,
Carrier: row.Carrier,
SdkVersion: row.SDKVersion,
Os: row.OS,
ResultIPs: row.ResultIPs,
Status: row.Status,
ErrorCode: row.ErrorCode,
CostMs: row.CostMs,
CreatedAt: row.CreatedAt,
Day: row.Day,
Summary: row.Summary,
}
}
func (s *HTTPDNSAccessLogsStore) buildConditions(f HTTPDNSAccessLogListFilter) []string {
conditions := []string{}
if day := strings.TrimSpace(f.Day); day != "" {
conditions = append(conditions, "day = '"+escapeString(day)+"'")
}
if f.ClusterId > 0 {
conditions = append(conditions, "cluster_id = "+strconv.FormatInt(f.ClusterId, 10))
}
if f.NodeId > 0 {
conditions = append(conditions, "node_id = "+strconv.FormatInt(f.NodeId, 10))
}
if appID := strings.TrimSpace(f.AppId); appID != "" {
conditions = append(conditions, "app_id = '"+escapeString(appID)+"'")
} else if len(f.AppIds) > 0 {
validAppIds := make([]string, 0, len(f.AppIds))
for _, appID := range f.AppIds {
appID = strings.TrimSpace(appID)
if len(appID) == 0 {
continue
}
validAppIds = append(validAppIds, "'"+escapeString(appID)+"'")
}
if len(validAppIds) == 0 {
conditions = append(conditions, "1 = 0")
} else {
conditions = append(conditions, "app_id IN ("+strings.Join(validAppIds, ",")+")")
}
}
if domain := strings.TrimSpace(f.Domain); domain != "" {
conditions = append(conditions, "domain = '"+escapeString(domain)+"'")
}
if status := strings.TrimSpace(f.Status); status != "" {
conditions = append(conditions, "status = '"+escapeString(status)+"'")
}
if keyword := strings.TrimSpace(f.Keyword); keyword != "" {
kw := escapeString(keyword)
conditions = append(conditions, "(summary LIKE '%"+kw+"%' OR app_name LIKE '%"+kw+"%' OR client_ip LIKE '%"+kw+"%' OR result_ips LIKE '%"+kw+"%')")
}
return conditions
}
func (s *HTTPDNSAccessLogsStore) tableName() string {
if s.client != nil && s.client.cfg != nil && s.client.cfg.Database != "" && s.client.cfg.Database != "default" {
return quoteIdent(s.client.cfg.Database) + "." + quoteIdent(httpDNSAccessLogsTable)
}
return quoteIdent(httpDNSAccessLogsTable)
}
func toString(value interface{}) string {
if value == nil {
return ""
}
switch v := value.(type) {
case string:
return v
case json.Number:
return v.String()
default:
return fmt.Sprintf("%v", v)
}
}
func toInt64(value interface{}) int64 {
if value == nil {
return 0
}
switch v := value.(type) {
case int:
return int64(v)
case int32:
return int64(v)
case int64:
return v
case uint32:
return int64(v)
case uint64:
return int64(v)
case float64:
return int64(v)
case json.Number:
n, _ := v.Int64()
return n
case string:
n, _ := strconv.ParseInt(v, 10, 64)
return n
default:
return 0
}
}

View File

@@ -39,6 +39,7 @@ type LogsIngestRow struct {
RequestBody string
ResponseHeaders string
ResponseBody string
Attrs string
}
// ListFilter 列表查询条件(与 ListHTTPAccessLogsRequest 对齐)
@@ -178,7 +179,7 @@ func (s *LogsIngestStore) List(ctx context.Context, f ListFilter) (rows []*LogsI
// 列表查询不 SELECT 大字段request_headers / request_body / response_headers / response_body
// 避免每次翻页读取 GB 级数据。详情查看时通过 FindByTraceId 单独获取。
query := fmt.Sprintf("SELECT timestamp, node_id, cluster_id, server_id, host, ip, method, path, status, bytes_in, bytes_out, cost_ms, ua, referer, log_type, trace_id, firewall_policy_id, firewall_rule_group_id, firewall_rule_set_id, firewall_rule_id FROM %s WHERE %s ORDER BY %s LIMIT %d",
query := fmt.Sprintf("SELECT timestamp, node_id, cluster_id, server_id, host, ip, method, path, status, bytes_in, bytes_out, cost_ms, ua, referer, log_type, trace_id, firewall_policy_id, firewall_rule_group_id, firewall_rule_set_id, firewall_rule_id, attrs FROM %s WHERE %s ORDER BY %s LIMIT %d",
table, where, orderBy, limit+1)
var rawRows []map[string]interface{}
@@ -225,7 +226,7 @@ func (s *LogsIngestStore) FindByTraceId(ctx context.Context, traceId string) (*L
}
table := quoteIdent("logs_ingest")
query := fmt.Sprintf("SELECT timestamp, node_id, cluster_id, server_id, host, ip, method, path, status, bytes_in, bytes_out, cost_ms, ua, referer, log_type, trace_id, firewall_policy_id, firewall_rule_group_id, firewall_rule_set_id, firewall_rule_id, request_headers, request_body, response_headers, response_body FROM %s WHERE trace_id = '%s' LIMIT 1",
query := fmt.Sprintf("SELECT timestamp, node_id, cluster_id, server_id, host, ip, method, path, status, bytes_in, bytes_out, cost_ms, ua, referer, log_type, trace_id, firewall_policy_id, firewall_rule_group_id, firewall_rule_set_id, firewall_rule_id, request_headers, request_body, response_headers, response_body, attrs FROM %s WHERE trace_id = '%s' LIMIT 1",
table, escapeString(traceId))
var rawRows []map[string]interface{}
@@ -333,6 +334,7 @@ func mapToLogsIngestRow(m map[string]interface{}) *LogsIngestRow {
r.RequestBody = str("request_body")
r.ResponseHeaders = str("response_headers")
r.ResponseBody = str("response_body")
r.Attrs = str("attrs")
return r
}
@@ -351,8 +353,8 @@ func RowToPB(r *LogsIngestRow) *pb.HTTPAccessLog {
RemoteAddr: r.IP,
RequestMethod: r.Method,
RequestPath: r.Path,
RequestURI: r.Path, // 前端使用 requestURI 显示完整路径
Scheme: "http", // 默认 http日志中未存储实际值
RequestURI: r.Path, // 前端使用 requestURI 显示完整路径
Scheme: "http", // 默认 http日志中未存储实际值
Proto: "HTTP/1.1", // 默认值,日志中未存储实际值
Status: int32(r.Status),
RequestLength: int64(r.BytesIn),
@@ -401,6 +403,14 @@ func RowToPB(r *LogsIngestRow) *pb.HTTPAccessLog {
a.RequestBody = []byte(r.RequestBody)
}
// 扩展属性cache.status 等)
if r.Attrs != "" {
var attrs map[string]string
if err := json.Unmarshal([]byte(r.Attrs), &attrs); err == nil && len(attrs) > 0 {
a.Attrs = attrs
}
}
return a
}

View File

@@ -1,7 +1,7 @@
package teaconst
const (
Version = "1.4.7" //1.3.9
Version = "1.5.1" //1.3.9
ProductName = "Edge API"
ProcessName = "edge-api"
@@ -17,6 +17,6 @@ const (
// 其他节点版本号,用来检测是否有需要升级的节点
NodeVersion = "1.4.7" //1.3.8.2
NodeVersion = "1.5.1" //1.3.8.2
)

View File

@@ -4,9 +4,9 @@
package teaconst
const (
DNSNodeVersion = "1.4.7" //1.3.8.2
UserNodeVersion = "1.4.7" //1.3.8.2
ReportNodeVersion = "0.1.5"
DNSNodeVersion = "1.5.1" //1.3.8.2
UserNodeVersion = "1.5.1" //1.3.8.2
ReportNodeVersion = "1.5.1"
DefaultMaxNodes int32 = 50
)

View File

@@ -19,7 +19,7 @@ func TestDB_Env(t *testing.T) {
func TestDB_Instance(t *testing.T) {
for i := 0; i < 10; i++ {
db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s")
db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s")
if err != nil {
t.Fatal(i, "open:", err)
}

View File

@@ -14,7 +14,7 @@ import (
func TestNewHTTPAccessLogManager(t *testing.T) {
var config = &dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge_log?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge_log?charset=utf8mb4&timeout=30s",
Prefix: "edge",
Connections: struct {
Pool int `yaml:"pool"`
@@ -45,7 +45,7 @@ func TestNewHTTPAccessLogManager(t *testing.T) {
func TestHTTPAccessLogManager_FindTableNames(t *testing.T) {
var config = &dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge_log?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge_log?charset=utf8mb4&timeout=30s",
Prefix: "edge",
Connections: struct {
Pool int `yaml:"pool"`
@@ -84,7 +84,7 @@ func TestHTTPAccessLogManager_FindTableNames(t *testing.T) {
func TestHTTPAccessLogManager_FindTables(t *testing.T) {
var config = &dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge_log?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge_log?charset=utf8mb4&timeout=30s",
Prefix: "edge",
Connections: struct {
Pool int `yaml:"pool"`
@@ -123,7 +123,7 @@ func TestHTTPAccessLogManager_FindTables(t *testing.T) {
func TestHTTPAccessLogManager_FindLastTable(t *testing.T) {
var config = &dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge_log?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge_log?charset=utf8mb4&timeout=30s",
Prefix: "edge",
Connections: struct {
Pool int `yaml:"pool"`
@@ -162,7 +162,7 @@ func TestHTTPAccessLogManager_FindLastTable(t *testing.T) {
func TestHTTPAccessLogManager_FindPartitionTable(t *testing.T) {
var config = &dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge_log?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge_log?charset=utf8mb4&timeout=30s",
Prefix: "edge",
Connections: struct {
Pool int `yaml:"pool"`

View File

@@ -0,0 +1,134 @@
package models
import (
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs"
"strings"
)
type HTTPDNSAccessLogDAO dbs.DAO
func NewHTTPDNSAccessLogDAO() *HTTPDNSAccessLogDAO {
return dbs.NewDAO(&HTTPDNSAccessLogDAO{
DAOObject: dbs.DAOObject{
DB: Tea.Env,
Table: "edgeHTTPDNSAccessLogs",
Model: new(HTTPDNSAccessLog),
PkName: "id",
},
}).(*HTTPDNSAccessLogDAO)
}
var SharedHTTPDNSAccessLogDAO *HTTPDNSAccessLogDAO
func init() {
dbs.OnReady(func() {
SharedHTTPDNSAccessLogDAO = NewHTTPDNSAccessLogDAO()
})
}
func (this *HTTPDNSAccessLogDAO) CreateLog(tx *dbs.Tx, log *HTTPDNSAccessLog) error {
var op = NewHTTPDNSAccessLogOperator()
op.RequestId = log.RequestId
op.ClusterId = log.ClusterId
op.NodeId = log.NodeId
op.AppId = log.AppId
op.AppName = log.AppName
op.Domain = log.Domain
op.QType = log.QType
op.ClientIP = log.ClientIP
op.ClientRegion = log.ClientRegion
op.Carrier = log.Carrier
op.SDKVersion = log.SDKVersion
op.OS = log.OS
op.ResultIPs = log.ResultIPs
op.Status = log.Status
op.ErrorCode = log.ErrorCode
op.CostMs = log.CostMs
op.CreatedAt = log.CreatedAt
op.Day = log.Day
op.Summary = log.Summary
return this.Save(tx, op)
}
func (this *HTTPDNSAccessLogDAO) BuildListQuery(tx *dbs.Tx, day string, clusterId int64, nodeId int64, appId string, domain string, status string, keyword string) *dbs.Query {
return this.BuildListQueryWithAppIds(tx, day, clusterId, nodeId, appId, nil, domain, status, keyword)
}
func (this *HTTPDNSAccessLogDAO) BuildListQueryWithAppIds(tx *dbs.Tx, day string, clusterId int64, nodeId int64, appId string, appIds []string, domain string, status string, keyword string) *dbs.Query {
query := this.Query(tx).DescPk()
if len(day) > 0 {
query = query.Attr("day", day)
}
if clusterId > 0 {
query = query.Attr("clusterId", clusterId)
}
if nodeId > 0 {
query = query.Attr("nodeId", nodeId)
}
if len(appIds) > 0 {
validAppIds := make([]string, 0, len(appIds))
for _, value := range appIds {
value = strings.TrimSpace(value)
if len(value) == 0 {
continue
}
validAppIds = append(validAppIds, value)
}
if len(validAppIds) == 0 {
query = query.Where("1 = 0")
} else {
query = query.Attr("appId", validAppIds)
}
}
if len(appId) > 0 {
query = query.Attr("appId", appId)
}
if len(domain) > 0 {
query = query.Attr("domain", domain)
}
if len(status) > 0 {
query = query.Attr("status", status)
}
if len(keyword) > 0 {
query = query.Where("(summary LIKE :kw OR appName LIKE :kw OR clientIP LIKE :kw OR resultIPs LIKE :kw)").Param("kw", "%"+keyword+"%")
}
return query
}
func (this *HTTPDNSAccessLogDAO) CountLogs(tx *dbs.Tx, day string, clusterId int64, nodeId int64, appId string, domain string, status string, keyword string) (int64, error) {
return this.BuildListQueryWithAppIds(tx, day, clusterId, nodeId, appId, nil, domain, status, keyword).Count()
}
func (this *HTTPDNSAccessLogDAO) ListLogs(tx *dbs.Tx, day string, clusterId int64, nodeId int64, appId string, domain string, status string, keyword string, offset int64, size int64) (result []*HTTPDNSAccessLog, err error) {
_, err = this.BuildListQueryWithAppIds(tx, day, clusterId, nodeId, appId, nil, domain, status, keyword).
Offset(offset).
Limit(size).
Slice(&result).
FindAll()
return
}
func (this *HTTPDNSAccessLogDAO) CountLogsWithAppIds(tx *dbs.Tx, day string, clusterId int64, nodeId int64, appId string, appIds []string, domain string, status string, keyword string) (int64, error) {
return this.BuildListQueryWithAppIds(tx, day, clusterId, nodeId, appId, appIds, domain, status, keyword).Count()
}
func (this *HTTPDNSAccessLogDAO) ListLogsWithAppIds(tx *dbs.Tx, day string, clusterId int64, nodeId int64, appId string, appIds []string, domain string, status string, keyword string, offset int64, size int64) (result []*HTTPDNSAccessLog, err error) {
_, err = this.BuildListQueryWithAppIds(tx, day, clusterId, nodeId, appId, appIds, domain, status, keyword).
Offset(offset).
Limit(size).
Slice(&result).
FindAll()
return
}
func (this *HTTPDNSAccessLogDAO) DeleteLogsWithAppId(tx *dbs.Tx, appId string) error {
if len(appId) == 0 {
return nil
}
_, err := this.Query(tx).
Attr("appId", appId).
Delete()
return err
}

View File

@@ -0,0 +1,138 @@
package models
import (
"github.com/iwind/TeaGo/dbs"
)
type HTTPDNSAccessLogHourlyStat struct {
Hour string `field:"hour"`
CountRequests int64 `field:"countRequests"`
}
type HTTPDNSAccessLogDailyStat struct {
Day string `field:"day"`
CountRequests int64 `field:"countRequests"`
}
type HTTPDNSAccessLogTopAppStat struct {
AppId string `field:"appId"`
AppName string `field:"appName"`
CountRequests int64 `field:"countRequests"`
}
type HTTPDNSAccessLogTopDomainStat struct {
Domain string `field:"domain"`
CountRequests int64 `field:"countRequests"`
}
type HTTPDNSAccessLogTopNodeStat struct {
ClusterId uint32 `field:"clusterId"`
NodeId uint32 `field:"nodeId"`
CountRequests int64 `field:"countRequests"`
}
func (this *HTTPDNSAccessLogDAO) FindHourlyStats(tx *dbs.Tx, hourFrom string, hourTo string) (result []*HTTPDNSAccessLogHourlyStat, err error) {
rows, _, err := this.Query(tx).
Result("CONCAT(day, LPAD(HOUR(FROM_UNIXTIME(createdAt)),2,'0')) AS hour", "COUNT(*) AS countRequests").
Where("CONCAT(day, LPAD(HOUR(FROM_UNIXTIME(createdAt)),2,'0')) BETWEEN :hourFrom AND :hourTo").
Param("hourFrom", hourFrom).
Param("hourTo", hourTo).
Group("hour").
FindOnes()
if err != nil {
return nil, err
}
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogHourlyStat{
Hour: row.GetString("hour"),
CountRequests: row.GetInt64("countRequests"),
})
}
return
}
func (this *HTTPDNSAccessLogDAO) FindDailyStats(tx *dbs.Tx, dayFrom string, dayTo string) (result []*HTTPDNSAccessLogDailyStat, err error) {
rows, _, err := this.Query(tx).
Result("day", "COUNT(*) AS countRequests").
Between("day", dayFrom, dayTo).
Group("day").
FindOnes()
if err != nil {
return nil, err
}
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogDailyStat{
Day: row.GetString("day"),
CountRequests: row.GetInt64("countRequests"),
})
}
return
}
func (this *HTTPDNSAccessLogDAO) ListTopApps(tx *dbs.Tx, dayFrom string, dayTo string, limit int64) (result []*HTTPDNSAccessLogTopAppStat, err error) {
rows, _, err := this.Query(tx).
Result("appId", "MAX(appName) AS appName", "COUNT(*) AS countRequests").
Between("day", dayFrom, dayTo).
Group("appId").
Desc("countRequests").
Limit(limit).
FindOnes()
if err != nil {
return nil, err
}
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogTopAppStat{
AppId: row.GetString("appId"),
AppName: row.GetString("appName"),
CountRequests: row.GetInt64("countRequests"),
})
}
return
}
func (this *HTTPDNSAccessLogDAO) ListTopDomains(tx *dbs.Tx, dayFrom string, dayTo string, limit int64) (result []*HTTPDNSAccessLogTopDomainStat, err error) {
rows, _, err := this.Query(tx).
Result("domain", "COUNT(*) AS countRequests").
Between("day", dayFrom, dayTo).
Where("domain != ''").
Group("domain").
Desc("countRequests").
Limit(limit).
FindOnes()
if err != nil {
return nil, err
}
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogTopDomainStat{
Domain: row.GetString("domain"),
CountRequests: row.GetInt64("countRequests"),
})
}
return
}
func (this *HTTPDNSAccessLogDAO) ListTopNodes(tx *dbs.Tx, dayFrom string, dayTo string, limit int64) (result []*HTTPDNSAccessLogTopNodeStat, err error) {
rows, _, err := this.Query(tx).
Result("MIN(clusterId) AS clusterId", "nodeId", "COUNT(*) AS countRequests").
Between("day", dayFrom, dayTo).
Group("nodeId").
Desc("countRequests").
Limit(limit).
FindOnes()
if err != nil {
return nil, err
}
for _, row := range rows {
result = append(result, &HTTPDNSAccessLogTopNodeStat{
ClusterId: row.GetUint32("clusterId"),
NodeId: row.GetUint32("nodeId"),
CountRequests: row.GetInt64("countRequests"),
})
}
return
}

View File

@@ -0,0 +1,52 @@
package models
// HTTPDNSAccessLog 访问日志
type HTTPDNSAccessLog struct {
Id uint64 `field:"id"` // ID
RequestId string `field:"requestId"` // 请求ID
ClusterId uint32 `field:"clusterId"` // 集群ID
NodeId uint32 `field:"nodeId"` // 节点ID
AppId string `field:"appId"` // AppID
AppName string `field:"appName"` // 应用名
Domain string `field:"domain"` // 域名
QType string `field:"qtype"` // 查询类型
ClientIP string `field:"clientIP"` // 客户端IP
ClientRegion string `field:"clientRegion"` // 客户端区域
Carrier string `field:"carrier"` // 运营商
SDKVersion string `field:"sdkVersion"` // SDK版本
OS string `field:"os"` // 系统
ResultIPs string `field:"resultIPs"` // 结果IP
Status string `field:"status"` // 状态
ErrorCode string `field:"errorCode"` // 错误码
CostMs int32 `field:"costMs"` // 耗时
CreatedAt uint64 `field:"createdAt"` // 创建时间
Day string `field:"day"` // YYYYMMDD
Summary string `field:"summary"` // 概要
}
type HTTPDNSAccessLogOperator struct {
Id any // ID
RequestId any // 请求ID
ClusterId any // 集群ID
NodeId any // 节点ID
AppId any // AppID
AppName any // 应用名
Domain any // 域名
QType any // 查询类型
ClientIP any // 客户端IP
ClientRegion any // 客户端区域
Carrier any // 运营商
SDKVersion any // SDK版本
OS any // 系统
ResultIPs any // 结果IP
Status any // 状态
ErrorCode any // 错误码
CostMs any // 耗时
CreatedAt any // 创建时间
Day any // YYYYMMDD
Summary any // 概要
}
func NewHTTPDNSAccessLogOperator() *HTTPDNSAccessLogOperator {
return &HTTPDNSAccessLogOperator{}
}

View File

@@ -0,0 +1,239 @@
package models
import (
"encoding/json"
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/types"
"time"
)
const (
HTTPDNSAppStateEnabled = 1
HTTPDNSAppStateDisabled = 0
HTTPDNSSNIModeFixedHide = "fixed_hide"
)
type HTTPDNSAppDAO dbs.DAO
func NewHTTPDNSAppDAO() *HTTPDNSAppDAO {
return dbs.NewDAO(&HTTPDNSAppDAO{
DAOObject: dbs.DAOObject{
DB: Tea.Env,
Table: "edgeHTTPDNSApps",
Model: new(HTTPDNSApp),
PkName: "id",
},
}).(*HTTPDNSAppDAO)
}
var SharedHTTPDNSAppDAO *HTTPDNSAppDAO
func init() {
dbs.OnReady(func() {
SharedHTTPDNSAppDAO = NewHTTPDNSAppDAO()
})
}
func (this *HTTPDNSAppDAO) CreateApp(tx *dbs.Tx, name string, appId string, clusterIdsJSON []byte, isOn bool, userId int64) (int64, error) {
var op = NewHTTPDNSAppOperator()
op.Name = name
op.AppId = appId
if len(clusterIdsJSON) > 0 {
op.ClusterIdsJSON = string(clusterIdsJSON)
} else {
op.ClusterIdsJSON = "[]"
}
op.IsOn = isOn
op.UserId = userId
op.SNIMode = HTTPDNSSNIModeFixedHide
op.CreatedAt = time.Now().Unix()
op.UpdatedAt = time.Now().Unix()
op.State = HTTPDNSAppStateEnabled
err := this.Save(tx, op)
if err != nil {
return 0, err
}
return types.Int64(op.Id), nil
}
func (this *HTTPDNSAppDAO) UpdateApp(tx *dbs.Tx, appDbId int64, name string, clusterIdsJSON []byte, isOn bool, userId int64) error {
var op = NewHTTPDNSAppOperator()
op.Id = appDbId
op.Name = name
if len(clusterIdsJSON) > 0 {
op.ClusterIdsJSON = string(clusterIdsJSON)
} else {
op.ClusterIdsJSON = "[]"
}
op.IsOn = isOn
op.UserId = userId
op.UpdatedAt = time.Now().Unix()
return this.Save(tx, op)
}
// ReadAppClusterIds reads cluster IDs from ClusterIdsJSON.
func (this *HTTPDNSAppDAO) ReadAppClusterIds(app *HTTPDNSApp) []int64 {
if app == nil {
return nil
}
if len(app.ClusterIdsJSON) > 0 {
var ids []int64
if err := json.Unmarshal([]byte(app.ClusterIdsJSON), &ids); err == nil && len(ids) > 0 {
return ids
}
}
return nil
}
func (this *HTTPDNSAppDAO) DisableApp(tx *dbs.Tx, appDbId int64) error {
_, err := this.Query(tx).
Pk(appDbId).
Set("state", HTTPDNSAppStateDisabled).
Update()
return err
}
func (this *HTTPDNSAppDAO) FindEnabledApp(tx *dbs.Tx, appDbId int64) (*HTTPDNSApp, error) {
one, err := this.Query(tx).
Pk(appDbId).
State(HTTPDNSAppStateEnabled).
Find()
if one == nil {
return nil, err
}
return one.(*HTTPDNSApp), nil
}
func (this *HTTPDNSAppDAO) FindEnabledAppWithUser(tx *dbs.Tx, appDbId int64, userId int64) (*HTTPDNSApp, error) {
one, err := this.Query(tx).
Pk(appDbId).
State(HTTPDNSAppStateEnabled).
Attr("userId", userId).
Find()
if one == nil {
return nil, err
}
return one.(*HTTPDNSApp), nil
}
func (this *HTTPDNSAppDAO) FindEnabledAppWithAppId(tx *dbs.Tx, appId string) (*HTTPDNSApp, error) {
one, err := this.Query(tx).
State(HTTPDNSAppStateEnabled).
Attr("appId", appId).
Find()
if one == nil {
return nil, err
}
return one.(*HTTPDNSApp), nil
}
func (this *HTTPDNSAppDAO) FindEnabledAppWithAppIdAndUser(tx *dbs.Tx, appId string, userId int64) (*HTTPDNSApp, error) {
one, err := this.Query(tx).
State(HTTPDNSAppStateEnabled).
Attr("appId", appId).
Attr("userId", userId).
Find()
if one == nil {
return nil, err
}
return one.(*HTTPDNSApp), nil
}
func (this *HTTPDNSAppDAO) FindLatestEnabledAppWithNameAndUser(tx *dbs.Tx, name string, userId int64) (*HTTPDNSApp, error) {
one, err := this.Query(tx).
State(HTTPDNSAppStateEnabled).
Attr("name", name).
Attr("userId", userId).
DescPk().
Find()
if one == nil {
return nil, err
}
return one.(*HTTPDNSApp), nil
}
func (this *HTTPDNSAppDAO) ListEnabledApps(tx *dbs.Tx, offset int64, size int64, keyword string) (result []*HTTPDNSApp, err error) {
query := this.Query(tx).
State(HTTPDNSAppStateEnabled).
AscPk()
if len(keyword) > 0 {
query = query.Where("(name LIKE :kw OR appId LIKE :kw)").Param("kw", "%"+keyword+"%")
}
if size > 0 {
query = query.Offset(offset).Limit(size)
}
_, err = query.Slice(&result).FindAll()
return
}
func (this *HTTPDNSAppDAO) ListEnabledAppsWithUser(tx *dbs.Tx, userId int64, offset int64, size int64, keyword string) (result []*HTTPDNSApp, err error) {
query := this.Query(tx).
State(HTTPDNSAppStateEnabled).
Attr("userId", userId).
AscPk()
if len(keyword) > 0 {
query = query.Where("(name LIKE :kw OR appId LIKE :kw)").Param("kw", "%"+keyword+"%")
}
if size > 0 {
query = query.Offset(offset).Limit(size)
}
_, err = query.Slice(&result).FindAll()
return
}
func (this *HTTPDNSAppDAO) CountEnabledApps(tx *dbs.Tx, keyword string) (int64, error) {
query := this.Query(tx).State(HTTPDNSAppStateEnabled)
if len(keyword) > 0 {
query = query.Where("(name LIKE :kw OR appId LIKE :kw)").Param("kw", "%"+keyword+"%")
}
return query.Count()
}
func (this *HTTPDNSAppDAO) CountEnabledAppsWithUser(tx *dbs.Tx, userId int64, keyword string) (int64, error) {
query := this.Query(tx).State(HTTPDNSAppStateEnabled).Attr("userId", userId)
if len(keyword) > 0 {
query = query.Where("(name LIKE :kw OR appId LIKE :kw)").Param("kw", "%"+keyword+"%")
}
return query.Count()
}
func (this *HTTPDNSAppDAO) FindAllEnabledApps(tx *dbs.Tx) (result []*HTTPDNSApp, err error) {
_, err = this.Query(tx).
State(HTTPDNSAppStateEnabled).
AscPk().
Slice(&result).
FindAll()
return
}
func (this *HTTPDNSAppDAO) FindAllEnabledAppsWithUser(tx *dbs.Tx, userId int64) (result []*HTTPDNSApp, err error) {
_, err = this.Query(tx).
State(HTTPDNSAppStateEnabled).
Attr("userId", userId).
AscPk().
Slice(&result).
FindAll()
return
}
func (this *HTTPDNSAppDAO) ListEnabledAppIdsWithUser(tx *dbs.Tx, userId int64) (result []string, err error) {
apps, err := this.FindAllEnabledAppsWithUser(tx, userId)
if err != nil {
return nil, err
}
result = make([]string, 0, len(apps))
for _, app := range apps {
if app == nil || len(app.AppId) == 0 {
continue
}
result = append(result, app.AppId)
}
return
}

View File

@@ -0,0 +1,34 @@
package models
// HTTPDNSApp maps to edgeHTTPDNSApps.
type HTTPDNSApp struct {
Id uint32 `field:"id"` // id
Name string `field:"name"` // app name
AppId string `field:"appId"` // external app id
IsOn bool `field:"isOn"` // enabled
ClusterIdsJSON string `field:"clusterIdsJSON"` // cluster ids json
SNIMode string `field:"sniMode"` // sni mode
UserId int64 `field:"userId"` // owner user id
CreatedAt uint64 `field:"createdAt"` // created unix ts
UpdatedAt uint64 `field:"updatedAt"` // updated unix ts
State uint8 `field:"state"` // state
}
// HTTPDNSAppOperator is used by DAO save/update.
type HTTPDNSAppOperator struct {
Id any // id
Name any // app name
AppId any // external app id
IsOn any // enabled
ClusterIdsJSON any // cluster ids json
SNIMode any // sni mode
UserId any // owner user id
CreatedAt any // created unix ts
UpdatedAt any // updated unix ts
State any // state
}
func NewHTTPDNSAppOperator() *HTTPDNSAppOperator {
return &HTTPDNSAppOperator{}
}

View File

@@ -0,0 +1,146 @@
package models
import (
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/rands"
"github.com/iwind/TeaGo/types"
"time"
)
const (
HTTPDNSAppSecretStateEnabled = 1
HTTPDNSAppSecretStateDisabled = 0
)
type HTTPDNSAppSecretDAO dbs.DAO
func NewHTTPDNSAppSecretDAO() *HTTPDNSAppSecretDAO {
return dbs.NewDAO(&HTTPDNSAppSecretDAO{
DAOObject: dbs.DAOObject{
DB: Tea.Env,
Table: "edgeHTTPDNSAppSecrets",
Model: new(HTTPDNSAppSecret),
PkName: "id",
},
}).(*HTTPDNSAppSecretDAO)
}
var SharedHTTPDNSAppSecretDAO *HTTPDNSAppSecretDAO
func init() {
dbs.OnReady(func() {
SharedHTTPDNSAppSecretDAO = NewHTTPDNSAppSecretDAO()
})
}
func (this *HTTPDNSAppSecretDAO) InitAppSecret(tx *dbs.Tx, appDbId int64, signEnabled bool) (string, uint64, error) {
signSecret := "ss_" + rands.HexString(12)
now := uint64(time.Now().Unix())
// 兼容历史数据:如果已存在(可能是停用状态)则直接恢复并更新,避免 UNIQUE(appId) 冲突
old, err := this.Query(tx).
Attr("appId", appDbId).
Find()
if err != nil {
return "", 0, err
}
if old != nil {
oldSecret := old.(*HTTPDNSAppSecret)
_, err = this.Query(tx).
Pk(oldSecret.Id).
Set("signEnabled", signEnabled).
Set("signSecret", signSecret).
Set("signUpdatedAt", now).
Set("updatedAt", now).
Set("state", HTTPDNSAppSecretStateEnabled).
Update()
return signSecret, now, err
}
var op = NewHTTPDNSAppSecretOperator()
op.AppId = appDbId
op.SignEnabled = signEnabled
op.SignSecret = signSecret
op.SignUpdatedAt = now
op.UpdatedAt = now
op.State = HTTPDNSAppSecretStateEnabled
err = this.Save(tx, op)
return signSecret, now, err
}
func (this *HTTPDNSAppSecretDAO) FindEnabledAppSecret(tx *dbs.Tx, appDbId int64) (*HTTPDNSAppSecret, error) {
one, err := this.Query(tx).
Attr("appId", appDbId).
State(HTTPDNSAppSecretStateEnabled).
Find()
if one == nil {
return nil, err
}
return one.(*HTTPDNSAppSecret), nil
}
func (this *HTTPDNSAppSecretDAO) UpdateSignEnabled(tx *dbs.Tx, appDbId int64, signEnabled bool) error {
_, err := this.Query(tx).
Attr("appId", appDbId).
State(HTTPDNSAppSecretStateEnabled).
Set("signEnabled", signEnabled).
Set("updatedAt", time.Now().Unix()).
Update()
return err
}
func (this *HTTPDNSAppSecretDAO) ResetSignSecret(tx *dbs.Tx, appDbId int64) (string, int64, error) {
signSecret := "ss_" + rands.HexString(12)
now := time.Now().Unix()
_, err := this.Query(tx).
Attr("appId", appDbId).
State(HTTPDNSAppSecretStateEnabled).
Set("signSecret", signSecret).
Set("signUpdatedAt", now).
Set("updatedAt", now).
Update()
if err != nil {
return "", 0, err
}
return signSecret, now, nil
}
func (this *HTTPDNSAppSecretDAO) FindSignEnabled(tx *dbs.Tx, appDbId int64) (bool, error) {
one, err := this.FindEnabledAppSecret(tx, appDbId)
if err != nil || one == nil {
return false, err
}
return one.SignEnabled, nil
}
func (this *HTTPDNSAppSecretDAO) FindSignSecretWithAppDbId(tx *dbs.Tx, appDbId int64) (string, error) {
return this.Query(tx).
Attr("appId", appDbId).
State(HTTPDNSAppSecretStateEnabled).
Result("signSecret").
FindStringCol("")
}
func (this *HTTPDNSAppSecretDAO) FindSignUpdatedAt(tx *dbs.Tx, appDbId int64) (int64, error) {
col, err := this.Query(tx).
Attr("appId", appDbId).
State(HTTPDNSAppSecretStateEnabled).
Result("signUpdatedAt").
FindCol(nil)
if err != nil {
return 0, err
}
return types.Int64(col), nil
}
func (this *HTTPDNSAppSecretDAO) DisableAppSecret(tx *dbs.Tx, appDbId int64) error {
_, err := this.Query(tx).
Attr("appId", appDbId).
State(HTTPDNSAppSecretStateEnabled).
Set("state", HTTPDNSAppSecretStateDisabled).
Set("updatedAt", time.Now().Unix()).
Update()
return err
}

View File

@@ -0,0 +1,26 @@
package models
// HTTPDNSAppSecret 应用验签密钥配置
type HTTPDNSAppSecret struct {
Id uint32 `field:"id"` // ID
AppId uint32 `field:"appId"` // 应用DB ID
SignEnabled bool `field:"signEnabled"` // 是否启用验签
SignSecret string `field:"signSecret"` // 验签密钥(当前先明文存储)
SignUpdatedAt uint64 `field:"signUpdatedAt"` // 验签密钥更新时间
UpdatedAt uint64 `field:"updatedAt"` // 修改时间
State uint8 `field:"state"` // 记录状态
}
type HTTPDNSAppSecretOperator struct {
Id any // ID
AppId any // 应用DB ID
SignEnabled any // 是否启用验签
SignSecret any // 验签密钥
SignUpdatedAt any // 验签密钥更新时间
UpdatedAt any // 修改时间
State any // 记录状态
}
func NewHTTPDNSAppSecretOperator() *HTTPDNSAppSecretOperator {
return &HTTPDNSAppSecretOperator{}
}

View File

@@ -0,0 +1,191 @@
package models
import (
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/types"
"time"
)
const (
HTTPDNSClusterStateEnabled = 1
HTTPDNSClusterStateDisabled = 0
)
type HTTPDNSClusterDAO dbs.DAO
func NewHTTPDNSClusterDAO() *HTTPDNSClusterDAO {
return dbs.NewDAO(&HTTPDNSClusterDAO{
DAOObject: dbs.DAOObject{
DB: Tea.Env,
Table: "edgeHTTPDNSClusters",
Model: new(HTTPDNSCluster),
PkName: "id",
},
}).(*HTTPDNSClusterDAO)
}
var SharedHTTPDNSClusterDAO *HTTPDNSClusterDAO
func init() {
dbs.OnReady(func() {
SharedHTTPDNSClusterDAO = NewHTTPDNSClusterDAO()
})
}
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).
Set("isDefault", false).
UpdateQuickly()
if err != nil {
return 0, err
}
}
var op = NewHTTPDNSClusterOperator()
op.Name = name
op.ServiceDomain = serviceDomain
op.DefaultTTL = defaultTTL
op.FallbackTimeoutMs = fallbackTimeoutMs
op.InstallDir = installDir
op.IsOn = isOn
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
if len(tlsPolicyJSON) > 0 {
op.TLSPolicy = tlsPolicyJSON
}
err := this.Save(tx, op)
if err != nil {
return 0, err
}
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, timeZone string) error {
if isDefault {
err := this.Query(tx).
State(HTTPDNSClusterStateEnabled).
Neq("id", clusterId).
Set("isDefault", false).
UpdateQuickly()
if err != nil {
return err
}
}
var op = NewHTTPDNSClusterOperator()
op.Id = clusterId
op.Name = name
op.ServiceDomain = serviceDomain
op.DefaultTTL = defaultTTL
op.FallbackTimeoutMs = fallbackTimeoutMs
op.InstallDir = installDir
op.IsOn = isOn
op.IsDefault = isDefault
op.AutoRemoteStart = autoRemoteStart
op.AccessLogIsOn = accessLogIsOn
op.TimeZone = timeZone
op.UpdatedAt = time.Now().Unix()
if len(tlsPolicyJSON) > 0 {
op.TLSPolicy = tlsPolicyJSON
}
return this.Save(tx, op)
}
func (this *HTTPDNSClusterDAO) DisableCluster(tx *dbs.Tx, clusterId int64) error {
_, err := this.Query(tx).
Pk(clusterId).
Set("state", HTTPDNSClusterStateDisabled).
Update()
return err
}
func (this *HTTPDNSClusterDAO) FindEnabledCluster(tx *dbs.Tx, clusterId int64) (*HTTPDNSCluster, error) {
one, err := this.Query(tx).
Pk(clusterId).
State(HTTPDNSClusterStateEnabled).
Find()
if one == nil {
return nil, err
}
return one.(*HTTPDNSCluster), nil
}
func (this *HTTPDNSClusterDAO) FindEnabledClusterName(tx *dbs.Tx, clusterId int64) (string, error) {
return this.Query(tx).
Pk(clusterId).
State(HTTPDNSClusterStateEnabled).
Result("name").
FindStringCol("")
}
func (this *HTTPDNSClusterDAO) ListEnabledClusters(tx *dbs.Tx, offset int64, size int64, keyword string) (result []*HTTPDNSCluster, err error) {
query := this.Query(tx).
State(HTTPDNSClusterStateEnabled).
AscPk()
if len(keyword) > 0 {
query = query.Where("(name LIKE :kw OR serviceDomain LIKE :kw)").Param("kw", "%"+keyword+"%")
}
if size > 0 {
query = query.Offset(offset).Limit(size)
}
_, err = query.Slice(&result).FindAll()
return
}
func (this *HTTPDNSClusterDAO) CountEnabledClusters(tx *dbs.Tx, keyword string) (int64, error) {
query := this.Query(tx).State(HTTPDNSClusterStateEnabled)
if len(keyword) > 0 {
query = query.Where("(name LIKE :kw OR serviceDomain LIKE :kw)").Param("kw", "%"+keyword+"%")
}
return query.Count()
}
func (this *HTTPDNSClusterDAO) FindAllEnabledClusters(tx *dbs.Tx) (result []*HTTPDNSCluster, err error) {
_, err = this.Query(tx).
State(HTTPDNSClusterStateEnabled).
AscPk().
Slice(&result).
FindAll()
return
}
func (this *HTTPDNSClusterDAO) FindDefaultPrimaryClusterId(tx *dbs.Tx) (int64, error) {
col, err := this.Query(tx).
State(HTTPDNSClusterStateEnabled).
Attr("isDefault", true).
Result("id").
AscPk().
FindCol(nil)
if err != nil {
return 0, err
}
if col == nil {
return 0, nil
}
return types.Int64(col), nil
}
func (this *HTTPDNSClusterDAO) UpdateDefaultCluster(tx *dbs.Tx, clusterId int64) error {
err := this.Query(tx).
State(HTTPDNSClusterStateEnabled).
Set("isDefault", false).
UpdateQuickly()
if err != nil {
return err
}
_, err = this.Query(tx).
Pk(clusterId).
State(HTTPDNSClusterStateEnabled).
Set("isDefault", true).
Update()
return err
}

View File

@@ -0,0 +1,44 @@
package models
import "github.com/iwind/TeaGo/dbs"
// HTTPDNSCluster HTTPDNS集群
type HTTPDNSCluster struct {
Id uint32 `field:"id"` // ID
Name string `field:"name"` // 集群名称
IsOn uint8 `field:"isOn"` // 是否启用tinyint unsigned
IsDefault uint8 `field:"isDefault"` // 默认集群tinyint unsigned
ServiceDomain string `field:"serviceDomain"` // 服务域名
DefaultTTL int32 `field:"defaultTTL"` // 默认TTL
FallbackTimeoutMs int32 `field:"fallbackTimeoutMs"` // 降级超时
InstallDir string `field:"installDir"` // 安装目录
TLSPolicy dbs.JSON `field:"tlsPolicy"` // TLS策略
AutoRemoteStart uint8 `field:"autoRemoteStart"` // 自动远程启动tinyint unsigned
AccessLogIsOn uint8 `field:"accessLogIsOn"` // 访问日志是否开启tinyint unsigned
TimeZone string `field:"timeZone"` // 时区
CreatedAt uint64 `field:"createdAt"` // 创建时间
UpdatedAt uint64 `field:"updatedAt"` // 修改时间
State uint8 `field:"state"` // 记录状态
}
type HTTPDNSClusterOperator struct {
Id any // ID
Name any // 集群名称
IsOn any // 是否启用
IsDefault any // 默认集群
ServiceDomain any // 服务域名
DefaultTTL any // 默认TTL
FallbackTimeoutMs any // 降级超时
InstallDir any // 安装目录
TLSPolicy any // TLS策略
AutoRemoteStart any // 自动远程启动
AccessLogIsOn any // 访问日志是否开启
TimeZone any // 时区
CreatedAt any // 创建时间
UpdatedAt any // 修改时间
State any // 记录状态
}
func NewHTTPDNSClusterOperator() *HTTPDNSClusterOperator {
return &HTTPDNSClusterOperator{}
}

View File

@@ -0,0 +1,143 @@
package models
import (
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/types"
"time"
)
const (
HTTPDNSCustomRuleStateEnabled = 1
HTTPDNSCustomRuleStateDisabled = 0
)
type HTTPDNSCustomRuleDAO dbs.DAO
func NewHTTPDNSCustomRuleDAO() *HTTPDNSCustomRuleDAO {
return dbs.NewDAO(&HTTPDNSCustomRuleDAO{
DAOObject: dbs.DAOObject{
DB: Tea.Env,
Table: "edgeHTTPDNSCustomRules",
Model: new(HTTPDNSCustomRule),
PkName: "id",
},
}).(*HTTPDNSCustomRuleDAO)
}
var SharedHTTPDNSCustomRuleDAO *HTTPDNSCustomRuleDAO
func init() {
dbs.OnReady(func() {
SharedHTTPDNSCustomRuleDAO = NewHTTPDNSCustomRuleDAO()
})
}
func (this *HTTPDNSCustomRuleDAO) CreateRule(tx *dbs.Tx, rule *HTTPDNSCustomRule) (int64, error) {
var op = NewHTTPDNSCustomRuleOperator()
op.AppId = rule.AppId
op.DomainId = rule.DomainId
op.RuleName = rule.RuleName
op.LineScope = rule.LineScope
op.LineCarrier = rule.LineCarrier
op.LineRegion = rule.LineRegion
op.LineProvince = rule.LineProvince
op.LineContinent = rule.LineContinent
op.LineCountry = rule.LineCountry
op.TTL = rule.TTL
op.IsOn = rule.IsOn
op.Priority = rule.Priority
op.UpdatedAt = time.Now().Unix()
op.State = HTTPDNSCustomRuleStateEnabled
err := this.Save(tx, op)
if err != nil {
return 0, err
}
return types.Int64(op.Id), nil
}
func (this *HTTPDNSCustomRuleDAO) UpdateRule(tx *dbs.Tx, rule *HTTPDNSCustomRule) error {
var op = NewHTTPDNSCustomRuleOperator()
op.Id = rule.Id
op.RuleName = rule.RuleName
op.LineScope = rule.LineScope
op.LineCarrier = rule.LineCarrier
op.LineRegion = rule.LineRegion
op.LineProvince = rule.LineProvince
op.LineContinent = rule.LineContinent
op.LineCountry = rule.LineCountry
op.TTL = rule.TTL
op.IsOn = rule.IsOn
op.Priority = rule.Priority
op.UpdatedAt = time.Now().Unix()
return this.Save(tx, op)
}
func (this *HTTPDNSCustomRuleDAO) DisableRule(tx *dbs.Tx, ruleId int64) error {
_, err := this.Query(tx).
Pk(ruleId).
Set("state", HTTPDNSCustomRuleStateDisabled).
Update()
return err
}
func (this *HTTPDNSCustomRuleDAO) DisableRulesWithAppId(tx *dbs.Tx, appDbId int64) error {
_, err := this.Query(tx).
Attr("appId", appDbId).
State(HTTPDNSCustomRuleStateEnabled).
Set("state", HTTPDNSCustomRuleStateDisabled).
Set("updatedAt", time.Now().Unix()).
Update()
return err
}
func (this *HTTPDNSCustomRuleDAO) UpdateRuleStatus(tx *dbs.Tx, ruleId int64, isOn bool) error {
_, err := this.Query(tx).
Pk(ruleId).
State(HTTPDNSCustomRuleStateEnabled).
Set("isOn", isOn).
Set("updatedAt", time.Now().Unix()).
Update()
return err
}
func (this *HTTPDNSCustomRuleDAO) FindEnabledRule(tx *dbs.Tx, ruleId int64) (*HTTPDNSCustomRule, error) {
one, err := this.Query(tx).
Pk(ruleId).
State(HTTPDNSCustomRuleStateEnabled).
Find()
if one == nil {
return nil, err
}
return one.(*HTTPDNSCustomRule), nil
}
func (this *HTTPDNSCustomRuleDAO) ListEnabledRulesWithDomainId(tx *dbs.Tx, domainId int64) (result []*HTTPDNSCustomRule, err error) {
_, err = this.Query(tx).
State(HTTPDNSCustomRuleStateEnabled).
Attr("domainId", domainId).
Asc("priority").
AscPk().
Slice(&result).
FindAll()
return
}
func (this *HTTPDNSCustomRuleDAO) ListEnabledRulesWithAppId(tx *dbs.Tx, appDbId int64) (result []*HTTPDNSCustomRule, err error) {
_, err = this.Query(tx).
State(HTTPDNSCustomRuleStateEnabled).
Attr("appId", appDbId).
Asc("priority").
AscPk().
Slice(&result).
FindAll()
return
}
func (this *HTTPDNSCustomRuleDAO) CountEnabledRulesWithDomainId(tx *dbs.Tx, domainId int64) (int64, error) {
return this.Query(tx).
State(HTTPDNSCustomRuleStateEnabled).
Attr("domainId", domainId).
Count()
}

View File

@@ -0,0 +1,42 @@
package models
// HTTPDNSCustomRule 自定义解析规则
type HTTPDNSCustomRule struct {
Id uint32 `field:"id"` // ID
AppId uint32 `field:"appId"` // 应用DB ID
DomainId uint32 `field:"domainId"` // 域名ID
RuleName string `field:"ruleName"` // 规则名称
LineScope string `field:"lineScope"` // 线路范围
LineCarrier string `field:"lineCarrier"` // 运营商
LineRegion string `field:"lineRegion"` // 区域
LineProvince string `field:"lineProvince"` // 省份
LineContinent string `field:"lineContinent"` // 大洲
LineCountry string `field:"lineCountry"` // 国家
TTL int32 `field:"ttl"` // TTL
IsOn bool `field:"isOn"` // 启用状态
Priority int32 `field:"priority"` // 优先级
UpdatedAt uint64 `field:"updatedAt"` // 修改时间
State uint8 `field:"state"` // 记录状态
}
type HTTPDNSCustomRuleOperator struct {
Id any // ID
AppId any // 应用DB ID
DomainId any // 域名ID
RuleName any // 规则名称
LineScope any // 线路范围
LineCarrier any // 运营商
LineRegion any // 区域
LineProvince any // 省份
LineContinent any // 大洲
LineCountry any // 国家
TTL any // TTL
IsOn any // 启用状态
Priority any // 优先级
UpdatedAt any // 修改时间
State any // 记录状态
}
func NewHTTPDNSCustomRuleOperator() *HTTPDNSCustomRuleOperator {
return &HTTPDNSCustomRuleOperator{}
}

View File

@@ -0,0 +1,69 @@
package models
import (
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/types"
)
const (
HTTPDNSCustomRuleRecordStateEnabled = 1
HTTPDNSCustomRuleRecordStateDisabled = 0
)
type HTTPDNSCustomRuleRecordDAO dbs.DAO
func NewHTTPDNSCustomRuleRecordDAO() *HTTPDNSCustomRuleRecordDAO {
return dbs.NewDAO(&HTTPDNSCustomRuleRecordDAO{
DAOObject: dbs.DAOObject{
DB: Tea.Env,
Table: "edgeHTTPDNSCustomRuleRecords",
Model: new(HTTPDNSCustomRuleRecord),
PkName: "id",
},
}).(*HTTPDNSCustomRuleRecordDAO)
}
var SharedHTTPDNSCustomRuleRecordDAO *HTTPDNSCustomRuleRecordDAO
func init() {
dbs.OnReady(func() {
SharedHTTPDNSCustomRuleRecordDAO = NewHTTPDNSCustomRuleRecordDAO()
})
}
func (this *HTTPDNSCustomRuleRecordDAO) CreateRecord(tx *dbs.Tx, ruleId int64, recordType string, recordValue string, weight int32, sort int32) (int64, error) {
var op = NewHTTPDNSCustomRuleRecordOperator()
op.RuleId = ruleId
op.RecordType = recordType
op.RecordValue = recordValue
op.Weight = weight
op.Sort = sort
op.State = HTTPDNSCustomRuleRecordStateEnabled
err := this.Save(tx, op)
if err != nil {
return 0, err
}
return types.Int64(op.Id), nil
}
func (this *HTTPDNSCustomRuleRecordDAO) DisableRecordsWithRuleId(tx *dbs.Tx, ruleId int64) error {
_, err := this.Query(tx).
Attr("ruleId", ruleId).
State(HTTPDNSCustomRuleRecordStateEnabled).
Set("state", HTTPDNSCustomRuleRecordStateDisabled).
Update()
return err
}
func (this *HTTPDNSCustomRuleRecordDAO) ListEnabledRecordsWithRuleId(tx *dbs.Tx, ruleId int64) (result []*HTTPDNSCustomRuleRecord, err error) {
_, err = this.Query(tx).
State(HTTPDNSCustomRuleRecordStateEnabled).
Attr("ruleId", ruleId).
Asc("sort").
AscPk().
Slice(&result).
FindAll()
return
}

View File

@@ -0,0 +1,26 @@
package models
// HTTPDNSCustomRuleRecord 自定义规则记录值
type HTTPDNSCustomRuleRecord struct {
Id uint32 `field:"id"` // ID
RuleId uint32 `field:"ruleId"` // 规则ID
RecordType string `field:"recordType"` // 记录类型
RecordValue string `field:"recordValue"` // 记录值
Weight int32 `field:"weight"` // 权重
Sort int32 `field:"sort"` // 顺序
State uint8 `field:"state"` // 记录状态
}
type HTTPDNSCustomRuleRecordOperator struct {
Id any // ID
RuleId any // 规则ID
RecordType any // 记录类型
RecordValue any // 记录值
Weight any // 权重
Sort any // 顺序
State any // 记录状态
}
func NewHTTPDNSCustomRuleRecordOperator() *HTTPDNSCustomRuleRecordOperator {
return &HTTPDNSCustomRuleRecordOperator{}
}

View File

@@ -0,0 +1,123 @@
package models
import (
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/types"
"strings"
"time"
)
const (
HTTPDNSDomainStateEnabled = 1
HTTPDNSDomainStateDisabled = 0
)
type HTTPDNSDomainDAO dbs.DAO
func NewHTTPDNSDomainDAO() *HTTPDNSDomainDAO {
return dbs.NewDAO(&HTTPDNSDomainDAO{
DAOObject: dbs.DAOObject{
DB: Tea.Env,
Table: "edgeHTTPDNSDomains",
Model: new(HTTPDNSDomain),
PkName: "id",
},
}).(*HTTPDNSDomainDAO)
}
var SharedHTTPDNSDomainDAO *HTTPDNSDomainDAO
func init() {
dbs.OnReady(func() {
SharedHTTPDNSDomainDAO = NewHTTPDNSDomainDAO()
})
}
func (this *HTTPDNSDomainDAO) CreateDomain(tx *dbs.Tx, appDbId int64, domain string, isOn bool) (int64, error) {
domain = strings.ToLower(strings.TrimSpace(domain))
var op = NewHTTPDNSDomainOperator()
op.AppId = appDbId
op.Domain = domain
op.IsOn = isOn
op.CreatedAt = time.Now().Unix()
op.UpdatedAt = time.Now().Unix()
op.State = HTTPDNSDomainStateEnabled
err := this.Save(tx, op)
if err != nil {
return 0, err
}
return types.Int64(op.Id), nil
}
func (this *HTTPDNSDomainDAO) DisableDomain(tx *dbs.Tx, domainId int64) error {
_, err := this.Query(tx).
Pk(domainId).
Set("state", HTTPDNSDomainStateDisabled).
Update()
return err
}
func (this *HTTPDNSDomainDAO) DisableDomainsWithAppId(tx *dbs.Tx, appDbId int64) error {
_, err := this.Query(tx).
Attr("appId", appDbId).
State(HTTPDNSDomainStateEnabled).
Set("state", HTTPDNSDomainStateDisabled).
Set("updatedAt", time.Now().Unix()).
Update()
return err
}
func (this *HTTPDNSDomainDAO) UpdateDomainStatus(tx *dbs.Tx, domainId int64, isOn bool) error {
_, err := this.Query(tx).
Pk(domainId).
State(HTTPDNSDomainStateEnabled).
Set("isOn", isOn).
Set("updatedAt", time.Now().Unix()).
Update()
return err
}
func (this *HTTPDNSDomainDAO) FindEnabledDomain(tx *dbs.Tx, domainId int64) (*HTTPDNSDomain, error) {
one, err := this.Query(tx).
Pk(domainId).
State(HTTPDNSDomainStateEnabled).
Find()
if one == nil {
return nil, err
}
return one.(*HTTPDNSDomain), nil
}
func (this *HTTPDNSDomainDAO) FindEnabledDomainWithAppAndName(tx *dbs.Tx, appDbId int64, domain string) (*HTTPDNSDomain, error) {
one, err := this.Query(tx).
State(HTTPDNSDomainStateEnabled).
Attr("appId", appDbId).
Attr("domain", strings.ToLower(strings.TrimSpace(domain))).
Find()
if one == nil {
return nil, err
}
return one.(*HTTPDNSDomain), nil
}
func (this *HTTPDNSDomainDAO) ListEnabledDomainsWithAppId(tx *dbs.Tx, appDbId int64, keyword string) (result []*HTTPDNSDomain, err error) {
query := this.Query(tx).
State(HTTPDNSDomainStateEnabled).
Attr("appId", appDbId).
AscPk()
if len(keyword) > 0 {
query = query.Where("domain LIKE :kw").Param("kw", "%"+keyword+"%")
}
_, err = query.Slice(&result).FindAll()
return
}
func (this *HTTPDNSDomainDAO) CountEnabledDomains(tx *dbs.Tx, appDbId int64) (int64, error) {
query := this.Query(tx).State(HTTPDNSDomainStateEnabled)
if appDbId > 0 {
query = query.Attr("appId", appDbId)
}
return query.Count()
}

View File

@@ -0,0 +1,26 @@
package models
// HTTPDNSDomain 应用绑定域名
type HTTPDNSDomain struct {
Id uint32 `field:"id"` // ID
AppId uint32 `field:"appId"` // 应用DB ID
Domain string `field:"domain"` // 业务域名
IsOn bool `field:"isOn"` // 是否启用
CreatedAt uint64 `field:"createdAt"` // 创建时间
UpdatedAt uint64 `field:"updatedAt"` // 修改时间
State uint8 `field:"state"` // 记录状态
}
type HTTPDNSDomainOperator struct {
Id any // ID
AppId any // 应用DB ID
Domain any // 业务域名
IsOn any // 是否启用
CreatedAt any // 创建时间
UpdatedAt any // 修改时间
State any // 记录状态
}
func NewHTTPDNSDomainOperator() *HTTPDNSDomainOperator {
return &HTTPDNSDomainOperator{}
}

View File

@@ -0,0 +1,327 @@
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"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/rands"
"github.com/iwind/TeaGo/types"
"time"
)
const (
HTTPDNSNodeStateEnabled = 1
HTTPDNSNodeStateDisabled = 0
)
type HTTPDNSNodeDAO dbs.DAO
func NewHTTPDNSNodeDAO() *HTTPDNSNodeDAO {
return dbs.NewDAO(&HTTPDNSNodeDAO{
DAOObject: dbs.DAOObject{
DB: Tea.Env,
Table: "edgeHTTPDNSNodes",
Model: new(HTTPDNSNode),
PkName: "id",
},
}).(*HTTPDNSNodeDAO)
}
var SharedHTTPDNSNodeDAO *HTTPDNSNodeDAO
func init() {
dbs.OnReady(func() {
SharedHTTPDNSNodeDAO = NewHTTPDNSNodeDAO()
})
}
// FindEnabledNodeIdWithUniqueId 鏍规嵁鍞竴ID鑾峰彇鍚敤涓殑HTTPDNS鑺傜偣ID
func (this *HTTPDNSNodeDAO) FindEnabledNodeIdWithUniqueId(tx *dbs.Tx, uniqueId string) (int64, error) {
return this.Query(tx).
Attr("uniqueId", uniqueId).
Attr("state", HTTPDNSNodeStateEnabled).
ResultPk().
FindInt64Col(0)
}
// 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)
err := SharedApiTokenDAO.CreateAPIToken(tx, uniqueId, secret, nodeconfigs.NodeRoleHTTPDNS)
if err != nil {
return 0, err
}
var op = NewHTTPDNSNodeOperator()
op.ClusterId = clusterId
op.Name = name
op.IsOn = isOn
op.IsUp = false
op.IsInstalled = false
op.IsActive = false
op.UniqueId = uniqueId
op.Secret = secret
op.InstallDir = installDir
op.CreatedAt = time.Now().Unix()
op.UpdatedAt = time.Now().Unix()
op.State = HTTPDNSNodeStateEnabled
err = this.Save(tx, op)
if err != nil {
return 0, err
}
return types.Int64(op.Id), nil
}
// UpdateNode 鏇存柊鑺傜偣
func (this *HTTPDNSNodeDAO) UpdateNode(tx *dbs.Tx, nodeId int64, name string, installDir string, isOn bool) error {
var op = NewHTTPDNSNodeOperator()
op.Id = nodeId
op.Name = name
op.InstallDir = installDir
op.IsOn = isOn
op.UpdatedAt = time.Now().Unix()
return this.Save(tx, op)
}
// DisableNode 绂佺敤鑺傜偣
func (this *HTTPDNSNodeDAO) DisableNode(tx *dbs.Tx, nodeId int64) error {
node, err := this.FindEnabledNode(tx, nodeId)
if err != nil {
return err
}
if node == nil {
return nil
}
_, err = this.Query(tx).
Pk(nodeId).
Set("state", HTTPDNSNodeStateDisabled).
Update()
if err != nil {
return err
}
_, err = SharedApiTokenDAO.Query(tx).
Attr("nodeId", node.UniqueId).
Attr("role", nodeconfigs.NodeRoleHTTPDNS).
Set("state", ApiTokenStateDisabled).
Update()
return err
}
// FindEnabledNode 鏌ユ壘鍚敤鑺傜偣
func (this *HTTPDNSNodeDAO) FindEnabledNode(tx *dbs.Tx, nodeId int64) (*HTTPDNSNode, error) {
one, err := this.Query(tx).
Pk(nodeId).
Attr("state", HTTPDNSNodeStateEnabled).
Find()
if one == nil {
return nil, err
}
return one.(*HTTPDNSNode), nil
}
// FindNodeClusterId 鏌ヨ鑺傜偣鎵€灞為泦缇D
func (this *HTTPDNSNodeDAO) FindNodeClusterId(tx *dbs.Tx, nodeId int64) (int64, error) {
return this.Query(tx).
Pk(nodeId).
Attr("state", HTTPDNSNodeStateEnabled).
Result("clusterId").
FindInt64Col(0)
}
// ListEnabledNodes 鍒楀嚭鑺傜偣
func (this *HTTPDNSNodeDAO) ListEnabledNodes(tx *dbs.Tx, clusterId int64) (result []*HTTPDNSNode, err error) {
query := this.Query(tx).
State(HTTPDNSNodeStateEnabled).
AscPk()
if clusterId > 0 {
query = query.Attr("clusterId", clusterId)
}
_, err = query.Slice(&result).FindAll()
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()
op.Id = nodeId
op.IsUp = isUp
op.IsInstalled = isInstalled
op.IsActive = isActive
op.UpdatedAt = time.Now().Unix()
if len(statusJSON) > 0 {
op.Status = statusJSON
}
if len(installStatusJSON) > 0 {
mergedStatusJSON, mergeErr := this.mergeInstallStatusJSON(tx, nodeId, installStatusJSON)
if mergeErr != nil {
return mergeErr
}
op.InstallStatus = mergedStatusJSON
}
return this.Save(tx, op)
}
// UpdateNodeInstallStatus 更新节点安装状态
func (this *HTTPDNSNodeDAO) UpdateNodeInstallStatus(tx *dbs.Tx, nodeId int64, installStatus *NodeInstallStatus) error {
if installStatus == nil {
return nil
}
// Read existing installStatus to preserve custom fields like 'ssh' and 'ipAddr'
raw, err := this.Query(tx).Pk(nodeId).Result("installStatus").FindBytesCol()
if err != nil {
return err
}
var m = map[string]interface{}{}
if len(raw) > 0 {
_ = json.Unmarshal(raw, &m)
}
// Overlay standard install status fields
statusData, err := json.Marshal(installStatus)
if err != nil {
return err
}
var newStatusMap = map[string]interface{}{}
_ = json.Unmarshal(statusData, &newStatusMap)
for k, v := range newStatusMap {
m[k] = v
}
// Re-marshal the merged map
mergedData, err := json.Marshal(m)
if err != nil {
return err
}
_, err = this.Query(tx).
Pk(nodeId).
Set("installStatus", mergedData).
Set("updatedAt", time.Now().Unix()).
Update()
return err
}
func (this *HTTPDNSNodeDAO) mergeInstallStatusJSON(tx *dbs.Tx, nodeId int64, patch []byte) ([]byte, error) {
if len(patch) == 0 {
return patch, nil
}
raw, err := this.Query(tx).Pk(nodeId).Result("installStatus").FindBytesCol()
if err != nil {
return nil, err
}
merged := map[string]interface{}{}
if len(raw) > 0 {
_ = json.Unmarshal(raw, &merged)
}
patchMap := map[string]interface{}{}
if len(patch) > 0 {
_ = json.Unmarshal(patch, &patchMap)
}
for k, v := range patchMap {
merged[k] = v
}
data, err := json.Marshal(merged)
if err != nil {
return nil, err
}
return data, nil
}
// FindNodeInstallStatus 读取节点安装状态
func (this *HTTPDNSNodeDAO) FindNodeInstallStatus(tx *dbs.Tx, nodeId int64) (*NodeInstallStatus, error) {
raw, err := this.Query(tx).
Pk(nodeId).
State(HTTPDNSNodeStateEnabled).
Result("installStatus").
FindBytesCol()
if err != nil {
return nil, err
}
if len(raw) == 0 {
return nil, nil
}
installStatus := &NodeInstallStatus{}
err = json.Unmarshal(raw, installStatus)
if err != nil {
return nil, err
}
return installStatus, nil
}
// 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).
State(HTTPDNSNodeStateEnabled).
Set("isInstalled", isInstalled).
Set("updatedAt", time.Now().Unix()).
Update()
return err
}

View File

@@ -0,0 +1,44 @@
package models
import "github.com/iwind/TeaGo/dbs"
// HTTPDNSNode HTTPDNS节点
type HTTPDNSNode struct {
Id uint32 `field:"id"` // ID
ClusterId uint32 `field:"clusterId"` // 集群ID
Name string `field:"name"` // 节点名称
IsOn bool `field:"isOn"` // 是否启用
IsUp bool `field:"isUp"` // 是否在线
IsInstalled bool `field:"isInstalled"` // 是否已安装
IsActive bool `field:"isActive"` // 是否活跃
UniqueId string `field:"uniqueId"` // 节点唯一ID
Secret string `field:"secret"` // 节点密钥
InstallDir string `field:"installDir"` // 安装目录
Status dbs.JSON `field:"status"` // 运行状态快照
InstallStatus dbs.JSON `field:"installStatus"` // 安装状态
CreatedAt uint64 `field:"createdAt"` // 创建时间
UpdatedAt uint64 `field:"updatedAt"` // 修改时间
State uint8 `field:"state"` // 记录状态
}
type HTTPDNSNodeOperator struct {
Id any // ID
ClusterId any // 集群ID
Name any // 节点名称
IsOn any // 是否启用
IsUp any // 是否在线
IsInstalled any // 是否已安装
IsActive any // 是否活跃
UniqueId any // 节点唯一ID
Secret any // 节点密钥
InstallDir any // 安装目录
Status any // 运行状态快照
InstallStatus any // 安装状态
CreatedAt any // 创建时间
UpdatedAt any // 修改时间
State any // 记录状态
}
func NewHTTPDNSNodeOperator() *HTTPDNSNodeOperator {
return &HTTPDNSNodeOperator{}
}

View File

@@ -0,0 +1,108 @@
package models
import (
"strconv"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/dbs"
)
type HTTPDNSRuntimeLogDAO dbs.DAO
func NewHTTPDNSRuntimeLogDAO() *HTTPDNSRuntimeLogDAO {
return dbs.NewDAO(&HTTPDNSRuntimeLogDAO{
DAOObject: dbs.DAOObject{
DB: Tea.Env,
Table: "edgeHTTPDNSRuntimeLogs",
Model: new(HTTPDNSRuntimeLog),
PkName: "id",
},
}).(*HTTPDNSRuntimeLogDAO)
}
var SharedHTTPDNSRuntimeLogDAO *HTTPDNSRuntimeLogDAO
func init() {
dbs.OnReady(func() {
SharedHTTPDNSRuntimeLogDAO = NewHTTPDNSRuntimeLogDAO()
})
}
func (this *HTTPDNSRuntimeLogDAO) CreateLog(tx *dbs.Tx, log *HTTPDNSRuntimeLog) error {
lastLog, err := this.Query(tx).
Result("id", "clusterId", "nodeId", "level", "type", "module", "description", "createdAt").
DescPk().
Find()
if err != nil {
return err
}
if lastLog != nil {
nodeLog := lastLog.(*HTTPDNSRuntimeLog)
if nodeLog.ClusterId == log.ClusterId &&
nodeLog.NodeId == log.NodeId &&
nodeLog.Level == log.Level &&
nodeLog.Type == log.Type &&
nodeLog.Module == log.Module &&
nodeLog.Description == log.Description &&
time.Now().Unix()-int64(nodeLog.CreatedAt) < 1800 {
count := log.Count
if count <= 0 {
count = 1
}
return this.Query(tx).
Pk(nodeLog.Id).
Set("count", dbs.SQL("count+"+strconv.FormatInt(count, 10))).
UpdateQuickly()
}
}
var op = NewHTTPDNSRuntimeLogOperator()
op.ClusterId = log.ClusterId
op.NodeId = log.NodeId
op.Level = log.Level
op.Type = log.Type
op.Module = log.Module
op.Description = log.Description
op.Count = log.Count
op.RequestId = log.RequestId
op.CreatedAt = log.CreatedAt
op.Day = log.Day
return this.Save(tx, op)
}
func (this *HTTPDNSRuntimeLogDAO) BuildListQuery(tx *dbs.Tx, day string, clusterId int64, nodeId int64, level string, keyword string) *dbs.Query {
query := this.Query(tx).DescPk()
if len(day) > 0 {
query = query.Attr("day", day)
}
if clusterId > 0 {
query = query.Attr("clusterId", clusterId)
}
if nodeId > 0 {
query = query.Attr("nodeId", nodeId)
}
if len(level) > 0 {
query = query.Attr("level", level)
}
if len(keyword) > 0 {
query = query.Where("(type LIKE :kw OR module LIKE :kw OR description LIKE :kw OR requestId LIKE :kw)").Param("kw", "%"+keyword+"%")
}
return query
}
func (this *HTTPDNSRuntimeLogDAO) CountLogs(tx *dbs.Tx, day string, clusterId int64, nodeId int64, level string, keyword string) (int64, error) {
return this.BuildListQuery(tx, day, clusterId, nodeId, level, keyword).Count()
}
func (this *HTTPDNSRuntimeLogDAO) ListLogs(tx *dbs.Tx, day string, clusterId int64, nodeId int64, level string, keyword string, offset int64, size int64) (result []*HTTPDNSRuntimeLog, err error) {
_, err = this.BuildListQuery(tx, day, clusterId, nodeId, level, keyword).
Offset(offset).
Limit(size).
Slice(&result).
FindAll()
return
}

View File

@@ -0,0 +1,34 @@
package models
// HTTPDNSRuntimeLog 运行日志
type HTTPDNSRuntimeLog struct {
Id uint64 `field:"id"` // ID
ClusterId uint32 `field:"clusterId"` // 集群ID
NodeId uint32 `field:"nodeId"` // 节点ID
Level string `field:"level"` // 级别
Type string `field:"type"` // 类型
Module string `field:"module"` // 模块
Description string `field:"description"` // 详情
Count int64 `field:"count"` // 次数
RequestId string `field:"requestId"` // 请求ID
CreatedAt uint64 `field:"createdAt"` // 创建时间
Day string `field:"day"` // YYYYMMDD
}
type HTTPDNSRuntimeLogOperator struct {
Id any // ID
ClusterId any // 集群ID
NodeId any // 节点ID
Level any // 级别
Type any // 类型
Module any // 模块
Description any // 详情
Count any // 次数
RequestId any // 请求ID
CreatedAt any // 创建时间
Day any // YYYYMMDD
}
func NewHTTPDNSRuntimeLogOperator() *HTTPDNSRuntimeLogOperator {
return &HTTPDNSRuntimeLogOperator{}
}

View File

@@ -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").

View File

@@ -15,35 +15,41 @@ import (
type NodeTaskType = string
const (
// CDN相关
// CDN鐩稿叧
NodeTaskTypeConfigChanged NodeTaskType = "configChanged" // 节点整体配置变化
NodeTaskTypeDDosProtectionChanged NodeTaskType = "ddosProtectionChanged" // 节点DDoS配置变更
NodeTaskTypeGlobalServerConfigChanged NodeTaskType = "globalServerConfigChanged" // 全局服务设置变化
NodeTaskTypeIPListDeleted NodeTaskType = "ipListDeleted" // IPList被删除
NodeTaskTypeIPItemChanged NodeTaskType = "ipItemChanged" // IP条目变更
NodeTaskTypeNodeVersionChanged NodeTaskType = "nodeVersionChanged" // 节点版本变化
NodeTaskTypeScriptsChanged NodeTaskType = "scriptsChanged" // 脚本配置变化
NodeTaskTypeNodeLevelChanged NodeTaskType = "nodeLevelChanged" // 节点级别变化
NodeTaskTypeUserServersStateChanged NodeTaskType = "userServersStateChanged" // 用户服务状态变化
NodeTaskTypeUAMPolicyChanged NodeTaskType = "uamPolicyChanged" // UAM策略变化
NodeTaskTypeHTTPPagesPolicyChanged NodeTaskType = "httpPagesPolicyChanged" // 自定义页面变化
NodeTaskTypeHTTPCCPolicyChanged NodeTaskType = "httpCCPolicyChanged" // CC策略变化
NodeTaskTypeHTTP3PolicyChanged NodeTaskType = "http3PolicyChanged" // HTTP3策略变化
NodeTaskTypeNetworkSecurityPolicyChanged NodeTaskType = "networkSecurityPolicyChanged" // 网络安全策略变化
NodeTaskTypeWebPPolicyChanged NodeTaskType = "webPPolicyChanged" // WebP策略变化
NodeTaskTypeUpdatingServers NodeTaskType = "updatingServers" // 更新一组服务
NodeTaskTypeTOAChanged NodeTaskType = "toaChanged" // TOA配置变化
NodeTaskTypePlanChanged NodeTaskType = "planChanged" // 套餐变化
NodeTaskTypeConfigChanged NodeTaskType = "configChanged" // 鑺傜偣鏁翠綋閰嶇疆鍙樺寲
NodeTaskTypeDDosProtectionChanged NodeTaskType = "ddosProtectionChanged" // 鑺傜偣DDoS閰嶇疆鍙樻洿
NodeTaskTypeGlobalServerConfigChanged NodeTaskType = "globalServerConfigChanged" // 鍏ㄥ眬鏈嶅姟璁剧疆鍙樺寲
NodeTaskTypeIPListDeleted NodeTaskType = "ipListDeleted" // IPList琚垹闄?
NodeTaskTypeIPItemChanged NodeTaskType = "ipItemChanged" // IP鏉$洰鍙樻洿
NodeTaskTypeNodeVersionChanged NodeTaskType = "nodeVersionChanged" // 鑺傜偣鐗堟湰鍙樺寲
NodeTaskTypeScriptsChanged NodeTaskType = "scriptsChanged" // 鑴氭湰閰嶇疆鍙樺寲
NodeTaskTypeNodeLevelChanged NodeTaskType = "nodeLevelChanged" // 鑺傜偣绾у埆鍙樺寲
NodeTaskTypeUserServersStateChanged NodeTaskType = "userServersStateChanged" // 鐢ㄦ埛鏈嶅姟鐘舵€佸彉鍖?
NodeTaskTypeUAMPolicyChanged NodeTaskType = "uamPolicyChanged" // UAM绛栫暐鍙樺寲
NodeTaskTypeHTTPPagesPolicyChanged NodeTaskType = "httpPagesPolicyChanged" // 鑷畾涔夐〉闈㈠彉鍖?
NodeTaskTypeHTTPCCPolicyChanged NodeTaskType = "httpCCPolicyChanged" // CC绛栫暐鍙樺寲
NodeTaskTypeHTTP3PolicyChanged NodeTaskType = "http3PolicyChanged" // HTTP3绛栫暐鍙樺寲
NodeTaskTypeNetworkSecurityPolicyChanged NodeTaskType = "networkSecurityPolicyChanged" // 缃戠粶瀹夊叏绛栫暐鍙樺寲
NodeTaskTypeWebPPolicyChanged NodeTaskType = "webPPolicyChanged" // WebP绛栫暐鍙樺寲
NodeTaskTypeUpdatingServers NodeTaskType = "updatingServers" // 鏇存柊涓€缁勬湇鍔?
NodeTaskTypeTOAChanged NodeTaskType = "toaChanged" // TOA閰嶇疆鍙樺寲
NodeTaskTypePlanChanged NodeTaskType = "planChanged" // 濂楅鍙樺寲
// NS相关
// NS鐩稿叧
NSNodeTaskTypeConfigChanged NodeTaskType = "nsConfigChanged"
NSNodeTaskTypeDomainChanged NodeTaskType = "nsDomainChanged"
NSNodeTaskTypeRecordChanged NodeTaskType = "nsRecordChanged"
NSNodeTaskTypeRouteChanged NodeTaskType = "nsRouteChanged"
NSNodeTaskTypeKeyChanged NodeTaskType = "nsKeyChanged"
NSNodeTaskTypeDDosProtectionChanged NodeTaskType = "nsDDoSProtectionChanged" // 节点DDoS配置变更
NSNodeTaskTypeDDosProtectionChanged NodeTaskType = "nsDDoSProtectionChanged" // 鑺傜偣DDoS閰嶇疆鍙樻洿
// HTTPDNS相关
HTTPDNSNodeTaskTypeConfigChanged NodeTaskType = "httpdnsConfigChanged"
HTTPDNSNodeTaskTypeAppChanged NodeTaskType = "httpdnsAppChanged"
HTTPDNSNodeTaskTypeDomainChanged NodeTaskType = "httpdnsDomainChanged"
HTTPDNSNodeTaskTypeRuleChanged NodeTaskType = "httpdnsRuleChanged"
HTTPDNSNodeTaskTypeTLSChanged NodeTaskType = "httpdnsTLSChanged"
)
type NodeTaskDAO dbs.DAO
@@ -67,15 +73,15 @@ func init() {
})
}
// CreateNodeTask 创建单个节点任务
// CreateNodeTask 鍒涘缓鍗曚釜鑺傜偣浠诲姟
func (this *NodeTaskDAO) CreateNodeTask(tx *dbs.Tx, role string, clusterId int64, nodeId int64, userId int64, serverId int64, taskType NodeTaskType) error {
if clusterId <= 0 || nodeId <= 0 {
return nil
}
var uniqueId = role + "@" + types.String(nodeId) + "@node@" + types.String(serverId) + "@" + taskType
// 用户信息
// 没有直接加入到 uniqueId 中,是为了兼容以前的字段值
// 鐢ㄦ埛淇℃伅
// 娌℃湁鐩存帴鍔犲叆鍒?uniqueId 涓紝鏄负浜嗗吋瀹逛互鍓嶇殑瀛楁鍊?
if userId > 0 {
uniqueId += "@" + types.String(userId)
}
@@ -113,7 +119,7 @@ func (this *NodeTaskDAO) CreateNodeTask(tx *dbs.Tx, role string, clusterId int64
return err
}
// CreateClusterTask 创建集群任务
// CreateClusterTask 鍒涘缓闆嗙兢浠诲姟
func (this *NodeTaskDAO) CreateClusterTask(tx *dbs.Tx, role string, clusterId int64, userId int64, serverId int64, taskType NodeTaskType) error {
if clusterId <= 0 {
return nil
@@ -121,8 +127,8 @@ func (this *NodeTaskDAO) CreateClusterTask(tx *dbs.Tx, role string, clusterId in
var uniqueId = role + "@" + types.String(clusterId) + "@" + types.String(serverId) + "@cluster@" + taskType
// 用户信息
// 没有直接加入到 uniqueId 中,是为了兼容以前的字段值
// 鐢ㄦ埛淇℃伅
// 娌℃湁鐩存帴鍔犲叆鍒?uniqueId 涓紝鏄负浜嗗吋瀹逛互鍓嶇殑瀛楁鍊?
if userId > 0 {
uniqueId += "@" + types.String(userId)
}
@@ -155,7 +161,7 @@ func (this *NodeTaskDAO) CreateClusterTask(tx *dbs.Tx, role string, clusterId in
return err
}
// ExtractNodeClusterTask 分解边缘节点集群任务
// ExtractNodeClusterTask 鍒嗚В杈圭紭鑺傜偣闆嗙兢浠诲姟
func (this *NodeTaskDAO) ExtractNodeClusterTask(tx *dbs.Tx, clusterId int64, userId int64, serverId int64, taskType NodeTaskType) error {
nodeIds, err := SharedNodeDAO.FindAllNodeIdsMatch(tx, clusterId, true, configutils.BoolStateYes)
if err != nil {
@@ -193,7 +199,7 @@ func (this *NodeTaskDAO) ExtractNodeClusterTask(tx *dbs.Tx, clusterId int64, use
return nil
}
// ExtractAllClusterTasks 分解所有集群任务
// ExtractAllClusterTasks 鍒嗚В鎵€鏈夐泦缇や换鍔?
func (this *NodeTaskDAO) ExtractAllClusterTasks(tx *dbs.Tx, role string) error {
ones, err := this.Query(tx).
Attr("role", role).
@@ -216,12 +222,17 @@ func (this *NodeTaskDAO) ExtractAllClusterTasks(tx *dbs.Tx, role string) error {
if err != nil {
return err
}
case nodeconfigs.NodeRoleHTTPDNS:
err = this.ExtractHTTPDNSClusterTask(tx, clusterId, one.(*NodeTask).Type)
if err != nil {
return err
}
}
}
return nil
}
// DeleteAllClusterTasks 删除集群所有相关任务
// DeleteAllClusterTasks 鍒犻櫎闆嗙兢鎵€鏈夌浉鍏充换鍔?
func (this *NodeTaskDAO) DeleteAllClusterTasks(tx *dbs.Tx, role string, clusterId int64) error {
_, err := this.Query(tx).
Attr("role", role).
@@ -230,7 +241,7 @@ func (this *NodeTaskDAO) DeleteAllClusterTasks(tx *dbs.Tx, role string, clusterI
return err
}
// DeleteNodeTasks 删除节点相关任务
// DeleteNodeTasks 鍒犻櫎鑺傜偣鐩稿叧浠诲姟
func (this *NodeTaskDAO) DeleteNodeTasks(tx *dbs.Tx, role string, nodeId int64) error {
_, err := this.Query(tx).
Attr("role", role).
@@ -239,13 +250,13 @@ func (this *NodeTaskDAO) DeleteNodeTasks(tx *dbs.Tx, role string, nodeId int64)
return err
}
// DeleteAllNodeTasks 删除所有节点相关任务
// DeleteAllNodeTasks 鍒犻櫎鎵€鏈夎妭鐐圭浉鍏充换鍔?
func (this *NodeTaskDAO) DeleteAllNodeTasks(tx *dbs.Tx) error {
return this.Query(tx).
DeleteQuickly()
}
// FindDoingNodeTasks 查询一个节点的所有任务
// FindDoingNodeTasks 鏌ヨ涓€涓妭鐐圭殑鎵€鏈変换鍔?
func (this *NodeTaskDAO) FindDoingNodeTasks(tx *dbs.Tx, role string, nodeId int64, version int64) (result []*NodeTask, err error) {
if nodeId <= 0 {
return
@@ -256,10 +267,10 @@ func (this *NodeTaskDAO) FindDoingNodeTasks(tx *dbs.Tx, role string, nodeId int6
UseIndex("nodeId").
Asc("version")
if version > 0 {
query.Lt("LENGTH(version)", 19) // 兼容以往版本
query.Lt("LENGTH(version)", 19) // 鍏煎浠ュ線鐗堟湰
query.Gt("version", version)
} else {
// 第一次访问时只取当前正在执行的或者执行失败的
// 绗竴娆¤闂椂鍙彇褰撳墠姝e湪鎵ц鐨勬垨鑰呮墽琛屽け璐ョ殑
query.Where("(isDone=0 OR (isDone=1 AND isOk=0))")
}
_, err = query.
@@ -268,10 +279,10 @@ func (this *NodeTaskDAO) FindDoingNodeTasks(tx *dbs.Tx, role string, nodeId int6
return
}
// UpdateNodeTaskDone 修改节点任务的完成状态
// UpdateNodeTaskDone 淇敼鑺傜偣浠诲姟鐨勫畬鎴愮姸鎬?
func (this *NodeTaskDAO) UpdateNodeTaskDone(tx *dbs.Tx, taskId int64, isOk bool, errorMessage string) error {
if isOk {
// 特殊任务删除
// 鐗规畩浠诲姟鍒犻櫎
taskType, err := this.Query(tx).
Pk(taskId).
Result("type").
@@ -286,7 +297,7 @@ func (this *NodeTaskDAO) UpdateNodeTaskDone(tx *dbs.Tx, taskId int64, isOk bool,
}
}
// 其他任务标记为完成
// 鍏朵粬浠诲姟鏍囪涓哄畬鎴?
var query = this.Query(tx).
Pk(taskId)
if !isOk {
@@ -305,7 +316,7 @@ func (this *NodeTaskDAO) UpdateNodeTaskDone(tx *dbs.Tx, taskId int64, isOk bool,
return err
}
// FindAllDoingTaskClusterIds 查找正在更新的集群IDs
// FindAllDoingTaskClusterIds 鏌ユ壘姝e湪鏇存柊鐨勯泦缇Ds
func (this *NodeTaskDAO) FindAllDoingTaskClusterIds(tx *dbs.Tx, role string) ([]int64, error) {
ones, _, err := this.Query(tx).
Result("DISTINCT(clusterId) AS clusterId").
@@ -322,7 +333,7 @@ func (this *NodeTaskDAO) FindAllDoingTaskClusterIds(tx *dbs.Tx, role string) ([]
return result, nil
}
// FindAllDoingNodeTasksWithClusterId 查询某个集群下所有的任务
// FindAllDoingNodeTasksWithClusterId 鏌ヨ鏌愪釜闆嗙兢涓嬫墍鏈夌殑浠诲姟
func (this *NodeTaskDAO) FindAllDoingNodeTasksWithClusterId(tx *dbs.Tx, role string, clusterId int64) (result []*NodeTask, err error) {
_, err = this.Query(tx).
Attr("role", role).
@@ -337,7 +348,7 @@ func (this *NodeTaskDAO) FindAllDoingNodeTasksWithClusterId(tx *dbs.Tx, role str
return
}
// FindAllDoingNodeIds 查询有任务的节点IDs
// FindAllDoingNodeIds 鏌ヨ鏈変换鍔$殑鑺傜偣IDs
func (this *NodeTaskDAO) FindAllDoingNodeIds(tx *dbs.Tx, role string) ([]int64, error) {
ones, err := this.Query(tx).
Result("DISTINCT(nodeId) AS nodeId").
@@ -356,7 +367,7 @@ func (this *NodeTaskDAO) FindAllDoingNodeIds(tx *dbs.Tx, role string) ([]int64,
return result, nil
}
// ExistsDoingNodeTasks 检查是否有正在执行的任务
// ExistsDoingNodeTasks 妫€鏌ユ槸鍚︽湁姝e湪鎵ц鐨勪换鍔?
func (this *NodeTaskDAO) ExistsDoingNodeTasks(tx *dbs.Tx, role string, excludeTypes []NodeTaskType) (bool, error) {
var query = this.Query(tx).
Attr("role", role).
@@ -370,7 +381,7 @@ func (this *NodeTaskDAO) ExistsDoingNodeTasks(tx *dbs.Tx, role string, excludeTy
return query.Exist()
}
// ExistsErrorNodeTasks 是否有错误的任务
// ExistsErrorNodeTasks 鏄惁鏈夐敊璇殑浠诲姟
func (this *NodeTaskDAO) ExistsErrorNodeTasks(tx *dbs.Tx, role string, excludeTypes []NodeTaskType) (bool, error) {
var query = this.Query(tx).
Attr("role", role).
@@ -383,7 +394,7 @@ func (this *NodeTaskDAO) ExistsErrorNodeTasks(tx *dbs.Tx, role string, excludeTy
return query.Exist()
}
// DeleteNodeTask 删除任务
// DeleteNodeTask 鍒犻櫎浠诲姟
func (this *NodeTaskDAO) DeleteNodeTask(tx *dbs.Tx, taskId int64) error {
_, err := this.Query(tx).
Pk(taskId).
@@ -391,7 +402,7 @@ func (this *NodeTaskDAO) DeleteNodeTask(tx *dbs.Tx, taskId int64) error {
return err
}
// CountDoingNodeTasks 计算正在执行的任务
// CountDoingNodeTasks 璁$畻姝e湪鎵ц鐨勪换鍔?
func (this *NodeTaskDAO) CountDoingNodeTasks(tx *dbs.Tx, role string) (int64, error) {
return this.Query(tx).
Attr("isDone", 0).
@@ -400,7 +411,7 @@ func (this *NodeTaskDAO) CountDoingNodeTasks(tx *dbs.Tx, role string) (int64, er
Count()
}
// FindNotifyingNodeTasks 查找需要通知的任务
// FindNotifyingNodeTasks 鏌ユ壘闇€瑕侀€氱煡鐨勪换鍔?
func (this *NodeTaskDAO) FindNotifyingNodeTasks(tx *dbs.Tx, role string, size int64) (result []*NodeTask, err error) {
_, err = this.Query(tx).
Attr("role", role).
@@ -413,7 +424,7 @@ func (this *NodeTaskDAO) FindNotifyingNodeTasks(tx *dbs.Tx, role string, size in
return
}
// UpdateTasksNotified 设置任务已通知
// UpdateTasksNotified 璁剧疆浠诲姟宸查€氱煡
func (this *NodeTaskDAO) UpdateTasksNotified(tx *dbs.Tx, taskIds []int64) error {
if len(taskIds) == 0 {
return nil
@@ -430,7 +441,7 @@ func (this *NodeTaskDAO) UpdateTasksNotified(tx *dbs.Tx, taskIds []int64) error
return nil
}
// 生成一个版本号
// 鐢熸垚涓€涓増鏈彿
func (this *NodeTaskDAO) increaseVersion(tx *dbs.Tx) (version int64, err error) {
return SharedSysLockerDAO.Increase(tx, "NODE_TASK_VERSION", 0)
}

View File

@@ -0,0 +1,47 @@
package models
import (
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/iwind/TeaGo/dbs"
)
// ExtractHTTPDNSClusterTask 分解HTTPDNS节点集群任务
func (this *NodeTaskDAO) ExtractHTTPDNSClusterTask(tx *dbs.Tx, clusterId int64, taskType NodeTaskType) error {
nodes, err := SharedHTTPDNSNodeDAO.ListEnabledNodes(tx, clusterId)
if err != nil {
return err
}
_, err = this.Query(tx).
Attr("role", nodeconfigs.NodeRoleHTTPDNS).
Attr("clusterId", clusterId).
Gt("nodeId", 0).
Attr("type", taskType).
Delete()
if err != nil {
return err
}
for _, node := range nodes {
if !node.IsOn {
continue
}
err = this.CreateNodeTask(tx, nodeconfigs.NodeRoleHTTPDNS, clusterId, int64(node.Id), 0, 0, taskType)
if err != nil {
return err
}
}
_, err = this.Query(tx).
Attr("role", nodeconfigs.NodeRoleHTTPDNS).
Attr("clusterId", clusterId).
Attr("nodeId", 0).
Attr("type", taskType).
Delete()
if err != nil {
return err
}
return nil
}

View File

@@ -221,6 +221,41 @@ func (this *NodeValueDAO) ListValuesForNSNodes(tx *dbs.Tx, item string, key stri
return
}
// ListValuesForHTTPDNSNodes 列出HTTPDNS节点相关的平均数据
func (this *NodeValueDAO) ListValuesForHTTPDNSNodes(tx *dbs.Tx, item string, key string, timeRange nodeconfigs.NodeValueRange) (result []*NodeValue, err error) {
query := this.Query(tx).
Attr("role", "httpdns").
Attr("item", item).
Result("AVG(JSON_EXTRACT(value, '$." + key + "')) AS value, MIN(createdAt) AS createdAt")
switch timeRange {
// TODO 支持更多的时间范围
case nodeconfigs.NodeValueRangeMinute:
fromMinute := timeutil.FormatTime("YmdHi", time.Now().Unix()-3600) // 一个小时之前的
query.Gte("minute", fromMinute)
query.Result("minute")
query.Group("minute")
default:
err = errors.New("invalid 'range' value: '" + timeRange + "'")
return
}
_, err = query.Slice(&result).
FindAll()
if err != nil {
return nil, err
}
for _, nodeValue := range result {
nodeValue.Value, _ = json.Marshal(maps.Map{
key: types.Float32(string(nodeValue.Value)),
})
}
return
}
// SumAllNodeValues 计算所有节点的某项参数值
func (this *NodeValueDAO) SumAllNodeValues(tx *dbs.Tx, role string, item nodeconfigs.NodeValueItem, param string, duration int32, durationUnit nodeconfigs.NodeValueDurationUnit) (total float64, avg float64, max float64, err error) {
if duration <= 0 {

View File

@@ -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).

View File

@@ -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 {

View File

@@ -246,7 +246,7 @@ func (this *UserDAO) CreateUser(tx *dbs.Tx, username string,
}
// UpdateUser 修改用户
func (this *UserDAO) UpdateUser(tx *dbs.Tx, userId int64, username string, password string, fullname string, mobile string, tel string, email string, remark string, isOn bool, nodeClusterId int64, bandwidthAlgo systemconfigs.BandwidthAlgo) error {
func (this *UserDAO) UpdateUser(tx *dbs.Tx, userId int64, username string, password string, fullname string, mobile string, tel string, email string, remark string, isOn bool, nodeClusterId int64, bandwidthAlgo systemconfigs.BandwidthAlgo, httpdnsClusterIdsJSON []byte) error {
if userId <= 0 {
return errors.New("invalid userId")
}
@@ -265,6 +265,11 @@ func (this *UserDAO) UpdateUser(tx *dbs.Tx, userId int64, username string, passw
op.ClusterId = nodeClusterId
op.BandwidthAlgo = bandwidthAlgo
op.IsOn = isOn
if len(httpdnsClusterIdsJSON) > 0 {
op.HttpdnsClusterIds = string(httpdnsClusterIdsJSON)
} else {
op.HttpdnsClusterIds = "[]"
}
err := this.Save(tx, op)
if err != nil {
return err
@@ -466,6 +471,21 @@ func (this *UserDAO) FindUserClusterId(tx *dbs.Tx, userId int64) (int64, error)
FindInt64Col(0)
}
// UpdateUserHttpdnsClusterIds 更新用户的HTTPDNS关联集群ID列表
func (this *UserDAO) UpdateUserHttpdnsClusterIds(tx *dbs.Tx, userId int64, httpdnsClusterIdsJSON []byte) error {
if userId <= 0 {
return errors.New("invalid userId")
}
if len(httpdnsClusterIdsJSON) == 0 {
httpdnsClusterIdsJSON = []byte("[]")
}
_, err := this.Query(tx).
Pk(userId).
Set("httpdnsClusterIds", httpdnsClusterIdsJSON).
Update()
return err
}
// UpdateUserFeatures 更新单个用户Features
func (this *UserDAO) UpdateUserFeatures(tx *dbs.Tx, userId int64, featuresJSON []byte) error {
if userId <= 0 {

View File

@@ -37,6 +37,7 @@ const (
UserField_BandwidthAlgo dbs.FieldName = "bandwidthAlgo" // 带宽算法
UserField_BandwidthModifier dbs.FieldName = "bandwidthModifier" // 带宽修正值
UserField_Lang dbs.FieldName = "lang" // 语言代号
UserField_HttpdnsClusterIds dbs.FieldName = "httpdnsClusterIds" // HTTPDNS关联集群ID列表
)
// User 用户
@@ -75,6 +76,7 @@ type User struct {
BandwidthAlgo string `field:"bandwidthAlgo"` // 带宽算法
BandwidthModifier float64 `field:"bandwidthModifier"` // 带宽修正值
Lang string `field:"lang"` // 语言代号
HttpdnsClusterIds dbs.JSON `field:"httpdnsClusterIds"` // HTTPDNS关联集群ID列表
}
type UserOperator struct {
@@ -112,6 +114,7 @@ type UserOperator struct {
BandwidthAlgo any // 带宽算法
BandwidthModifier any // 带宽修正值
Lang any // 语言代号
HttpdnsClusterIds any // HTTPDNS关联集群ID列表
}
func NewUserOperator() *UserOperator {

View File

@@ -19,7 +19,7 @@ func TestIsLocalAddr(t *testing.T) {
a.IsTrue(dbutils.IsLocalAddr("127.0.0.1"))
a.IsTrue(dbutils.IsLocalAddr("localhost"))
a.IsTrue(dbutils.IsLocalAddr("::1"))
a.IsTrue(dbutils.IsLocalAddr("127.0.0.1:3306"))
a.IsTrue(dbutils.IsLocalAddr("127.0.0.1:3308"))
a.IsFalse(dbutils.IsLocalAddr("192.168.2.200"))
a.IsFalse(dbutils.IsLocalAddr("192.168.2.200:3306"))
}

View File

@@ -15,8 +15,9 @@ var SharedDeployManager = NewDeployManager()
type DeployManager struct {
dir string
nodeFiles []*DeployFile
nsNodeFiles []*DeployFile
nodeFiles []*DeployFile
nsNodeFiles []*DeployFile
httpdnsNodeFiles []*DeployFile
locker sync.Mutex
}
@@ -28,6 +29,7 @@ func NewDeployManager() *DeployManager {
}
manager.LoadNodeFiles()
manager.LoadNSNodeFiles()
manager.LoadHTTPDNSNodeFiles()
return manager
}
@@ -141,6 +143,76 @@ func (this *DeployManager) FindNSNodeFile(os string, arch string) *DeployFile {
return nil
}
// LoadHTTPDNSNodeFiles 加载所有HTTPDNS节点安装文件
func (this *DeployManager) LoadHTTPDNSNodeFiles() []*DeployFile {
this.locker.Lock()
defer this.locker.Unlock()
if len(this.httpdnsNodeFiles) > 0 {
return this.httpdnsNodeFiles
}
var keyMap = map[string]*DeployFile{} // key => File
var reg = regexp.MustCompile(`^edge-httpdns-(\w+)-(\w+)-v([0-9.]+)\.zip$`)
for _, file := range files.NewFile(this.dir).List() {
var name = file.Name()
if !reg.MatchString(name) {
continue
}
var matches = reg.FindStringSubmatch(name)
var osName = matches[1]
var arch = matches[2]
var version = matches[3]
var key = osName + "_" + arch
oldFile, ok := keyMap[key]
if ok && stringutil.VersionCompare(oldFile.Version, version) > 0 {
continue
}
keyMap[key] = &DeployFile{
OS: osName,
Arch: arch,
Version: version,
Path: file.Path(),
}
}
var result = []*DeployFile{}
for _, v := range keyMap {
result = append(result, v)
}
this.httpdnsNodeFiles = result
return result
}
// FindHTTPDNSNodeFile 查找特定平台的HTTPDNS节点安装文件
func (this *DeployManager) FindHTTPDNSNodeFile(os string, arch string) *DeployFile {
for _, file := range this.LoadHTTPDNSNodeFiles() {
if file.OS == os && file.Arch == arch {
return file
}
}
return nil
}
// LatestNodeVersion 获取部署目录中边缘节点的最新版本号
func (this *DeployManager) LatestNodeVersion() string {
return latestVersion(this.LoadNodeFiles())
}
// LatestNSNodeVersion 获取部署目录中DNS节点的最新版本号
func (this *DeployManager) LatestNSNodeVersion() string {
return latestVersion(this.LoadNSNodeFiles())
}
// LatestHTTPDNSNodeVersion 获取部署目录中HTTPDNS节点的最新版本号
func (this *DeployManager) LatestHTTPDNSNodeVersion() string {
return latestVersion(this.LoadHTTPDNSNodeFiles())
}
// Reload 重置缓存
func (this *DeployManager) Reload() {
this.locker.Lock()
@@ -148,4 +220,15 @@ func (this *DeployManager) Reload() {
this.nodeFiles = nil
this.nsNodeFiles = nil
this.httpdnsNodeFiles = nil
}
func latestVersion(files []*DeployFile) string {
var latest string
for _, f := range files {
if len(latest) == 0 || stringutil.VersionCompare(f.Version, latest) > 0 {
latest = f.Version
}
}
return latest
}

View File

@@ -1,832 +0,0 @@
package installers
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
slashpath "path"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/iwind/TeaGo/Tea"
)
const (
fluentBitConfigDir = "/etc/fluent-bit"
fluentBitStorageDir = "/var/lib/fluent-bit/storage"
fluentBitMainConfigFile = "/etc/fluent-bit/fluent-bit.conf"
fluentBitParsersFile = "/etc/fluent-bit/parsers.conf"
fluentBitManagedMetaFile = "/etc/fluent-bit/.edge-managed.json"
fluentBitManagedEnvFile = "/etc/fluent-bit/.edge-managed.env"
fluentBitDropInDir = "/etc/systemd/system/fluent-bit.service.d"
fluentBitDropInFile = "/etc/systemd/system/fluent-bit.service.d/edge-managed.conf"
fluentBitServiceName = "fluent-bit"
fluentBitDefaultBinPath = "/opt/fluent-bit/bin/fluent-bit"
fluentBitLocalPackagesRoot = "packages"
fluentBitHTTPPathPattern = "/var/log/edge/edge-node/*.log"
fluentBitDNSPathPattern = "/var/log/edge/edge-dns/*.log"
fluentBitManagedMarker = "managed-by-edgeapi"
fluentBitRoleNode = "node"
fluentBitRoleDNS = "dns"
)
var errFluentBitLocalPackageNotFound = errors.New("fluent-bit local package not found")
var fluentBitPackageFileMapping = map[string]string{
"ubuntu22.04-amd64": "fluent-bit_4.2.2_amd64.deb",
"ubuntu22.04-arm64": "fluent-bit_4.2.2_arm64.deb",
"amzn2023-amd64": "fluent-bit-4.2.2-1.x86_64.rpm",
"amzn2023-arm64": "fluent-bit-4.2.2-1.aarch64.rpm",
}
type fluentBitManagedMeta struct {
Roles []string `json:"roles"`
Hash string `json:"hash"`
UpdatedAt int64 `json:"updatedAt"`
SourceVersion string `json:"sourceVersion"`
}
type fluentBitDesiredConfig struct {
Roles []string
ClickHouse *systemconfigs.ClickHouseSetting
HTTPPathPattern string
DNSPathPattern string
}
// SetupFluentBit 安装并托管 Fluent Bit 配置(离线包 + 平台渲染配置)。
func (this *BaseInstaller) SetupFluentBit(role nodeconfigs.NodeRole) error {
if this.client == nil {
return errors.New("ssh client is nil")
}
uname := this.uname()
if !strings.Contains(uname, "Linux") {
return nil
}
tempDir := strings.TrimRight(this.client.UserHome(), "/") + "/.edge-fluent-bit"
_, _, _ = this.client.Exec("mkdir -p " + shQuote(tempDir))
defer func() {
_, _, _ = this.client.Exec("rm -rf " + shQuote(tempDir))
}()
if err := this.ensureFluentBitInstalled(tempDir); err != nil {
return err
}
_, stderr, err := this.client.Exec("mkdir -p " + shQuote(fluentBitConfigDir) + " " + shQuote(fluentBitStorageDir))
if err != nil {
return fmt.Errorf("prepare fluent-bit directories failed: %w, stderr: %s", err, stderr)
}
parserContent, err := this.readLocalParsersContent()
if err != nil {
return err
}
existingMeta, err := this.readManagedMeta()
if err != nil {
return err
}
mergedRoles, err := mergeManagedRoles(existingMeta, role)
if err != nil {
return err
}
desiredConfig, err := this.buildDesiredFluentBitConfig(mergedRoles)
if err != nil {
return err
}
configChanged, err := this.applyManagedConfig(tempDir, desiredConfig, parserContent, existingMeta)
if err != nil {
return err
}
binPath, err := this.lookupFluentBitBinPath()
if err != nil {
return err
}
if err := this.ensureFluentBitService(tempDir, binPath, configChanged); err != nil {
return err
}
return nil
}
func (this *BaseInstaller) ensureFluentBitInstalled(tempDir string) error {
binPath, _ := this.lookupFluentBitBinPath()
if binPath != "" {
return nil
}
platformKey, packageName, arch, err := this.detectRemotePlatformAndPackage()
if err != nil {
return fmt.Errorf("detect fluent-bit platform failed: %w", err)
}
if err := this.installFluentBitFromLocalPackage(tempDir, arch, packageName); err != nil {
if errors.Is(err, errFluentBitLocalPackageNotFound) {
expectedPath := filepath.Join("deploy", "fluent-bit", fluentBitLocalPackagesRoot, "linux-"+arch, packageName)
return fmt.Errorf("install fluent-bit failed: local package missing for platform '%s', expected '%s'", platformKey, expectedPath)
}
return fmt.Errorf("install fluent-bit from local package failed: %w", err)
}
binPath, err = this.lookupFluentBitBinPath()
if err != nil {
return err
}
if binPath == "" {
return errors.New("fluent-bit binary not found after local package install")
}
_, stderr, err := this.client.Exec(binPath + " --version")
if err != nil {
return fmt.Errorf("verify fluent-bit version failed: %w, stderr: %s", err, stderr)
}
return nil
}
func (this *BaseInstaller) installFluentBitFromLocalPackage(tempDir string, arch string, packageName string) error {
packageDir := filepath.Join(Tea.Root, "deploy", "fluent-bit", fluentBitLocalPackagesRoot, "linux-"+arch)
localPackagePath := filepath.Join(packageDir, packageName)
if _, err := os.Stat(localPackagePath); err != nil {
if os.IsNotExist(err) {
return errFluentBitLocalPackageNotFound
}
return fmt.Errorf("check local package failed: %w", err)
}
remotePackagePath := tempDir + "/" + filepath.Base(localPackagePath)
if err := this.client.Copy(localPackagePath, remotePackagePath, 0644); err != nil {
return fmt.Errorf("upload local package failed: %w", err)
}
var installCmd string
lowerName := strings.ToLower(localPackagePath)
switch {
case strings.HasSuffix(lowerName, ".deb"):
installCmd = "dpkg -i " + shQuote(remotePackagePath)
case strings.HasSuffix(lowerName, ".rpm"):
installCmd = "rpm -Uvh --force " + shQuote(remotePackagePath) + " || rpm -ivh --force " + shQuote(remotePackagePath)
case strings.HasSuffix(lowerName, ".tar.gz") || strings.HasSuffix(lowerName, ".tgz"):
extractDir := tempDir + "/extract"
installCmd = "rm -rf " + shQuote(extractDir) + "; mkdir -p " + shQuote(extractDir) + "; tar -xzf " + shQuote(remotePackagePath) + " -C " + shQuote(extractDir) + "; " +
"bin=$(find " + shQuote(extractDir) + " -type f -name fluent-bit | head -n 1); " +
"if [ -z \"$bin\" ]; then exit 3; fi; " +
"mkdir -p /opt/fluent-bit/bin /usr/local/bin; " +
"install -m 0755 \"$bin\" /opt/fluent-bit/bin/fluent-bit; " +
"ln -sf /opt/fluent-bit/bin/fluent-bit /usr/local/bin/fluent-bit"
default:
return fmt.Errorf("unsupported local package format: %s", packageName)
}
_, stderr, err := this.client.Exec(installCmd)
if err != nil {
return fmt.Errorf("install fluent-bit local package '%s' failed: %w, stderr: %s", filepath.Base(localPackagePath), err, stderr)
}
return nil
}
func (this *BaseInstaller) detectRemotePlatformAndPackage() (platformKey string, packageName string, arch string, err error) {
arch, err = this.detectRemoteLinuxArch()
if err != nil {
return "", "", "", err
}
releaseData, stderr, err := this.client.Exec("cat /etc/os-release")
if err != nil {
return "", "", "", fmt.Errorf("read /etc/os-release failed: %w, stderr: %s", err, stderr)
}
if strings.TrimSpace(releaseData) == "" {
return "", "", "", errors.New("/etc/os-release is empty")
}
releaseMap := parseOSRelease(releaseData)
id := strings.ToLower(strings.TrimSpace(releaseMap["ID"]))
versionID := strings.TrimSpace(releaseMap["VERSION_ID"])
var distro string
switch {
case id == "ubuntu" && strings.HasPrefix(versionID, "22.04"):
distro = "ubuntu22.04"
case id == "amzn" && strings.HasPrefix(versionID, "2023"):
distro = "amzn2023"
default:
return "", "", "", fmt.Errorf("unsupported linux platform id='%s' version='%s'", id, versionID)
}
platformKey = distro + "-" + arch
packageName, ok := fluentBitPackageFileMapping[platformKey]
if !ok {
return "", "", "", fmt.Errorf("no local package mapping for platform '%s'", platformKey)
}
return platformKey, packageName, arch, nil
}
func parseOSRelease(content string) map[string]string {
result := map[string]string{}
lines := strings.Split(content, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") || !strings.Contains(line, "=") {
continue
}
parts := strings.SplitN(line, "=", 2)
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
value = strings.Trim(value, "\"")
result[key] = value
}
return result
}
func (this *BaseInstaller) detectRemoteLinuxArch() (string, error) {
stdout, stderr, err := this.client.Exec("uname -m")
if err != nil {
return "", fmt.Errorf("detect remote arch failed: %w, stderr: %s", err, stderr)
}
arch := strings.ToLower(strings.TrimSpace(stdout))
switch arch {
case "x86_64", "amd64":
return "amd64", nil
case "aarch64", "arm64":
return "arm64", nil
default:
return arch, nil
}
}
func (this *BaseInstaller) lookupFluentBitBinPath() (string, error) {
stdout, stderr, err := this.client.Exec("if command -v fluent-bit >/dev/null 2>&1; then command -v fluent-bit; elif [ -x " + fluentBitDefaultBinPath + " ]; then echo " + fluentBitDefaultBinPath + "; fi")
if err != nil {
return "", fmt.Errorf("lookup fluent-bit binary failed: %w, stderr: %s", err, stderr)
}
return strings.TrimSpace(stdout), nil
}
func (this *BaseInstaller) readLocalParsersContent() (string, error) {
parsersPath := filepath.Join(Tea.Root, "deploy", "fluent-bit", "parsers.conf")
data, err := os.ReadFile(parsersPath)
if err != nil {
return "", fmt.Errorf("read local parsers config failed: %w", err)
}
return string(data), nil
}
func (this *BaseInstaller) readManagedMeta() (*fluentBitManagedMeta, error) {
exists, err := this.remoteFileExists(fluentBitManagedMetaFile)
if err != nil {
return nil, err
}
if !exists {
return nil, nil
}
content, stderr, err := this.client.Exec("cat " + shQuote(fluentBitManagedMetaFile))
if err != nil {
return nil, fmt.Errorf("read fluent-bit managed metadata failed: %w, stderr: %s", err, stderr)
}
if strings.TrimSpace(content) == "" {
return nil, nil
}
meta := &fluentBitManagedMeta{}
if err := json.Unmarshal([]byte(content), meta); err != nil {
return nil, fmt.Errorf("decode fluent-bit managed metadata failed: %w", err)
}
meta.Roles = normalizeRoles(meta.Roles)
return meta, nil
}
func mergeManagedRoles(meta *fluentBitManagedMeta, role nodeconfigs.NodeRole) ([]string, error) {
roleName, err := mapNodeRole(role)
if err != nil {
return nil, err
}
roleSet := map[string]struct{}{}
if meta != nil {
for _, r := range normalizeRoles(meta.Roles) {
roleSet[r] = struct{}{}
}
}
roleSet[roleName] = struct{}{}
roles := make([]string, 0, len(roleSet))
for r := range roleSet {
roles = append(roles, r)
}
sort.Strings(roles)
return roles, nil
}
func mapNodeRole(role nodeconfigs.NodeRole) (string, error) {
switch role {
case nodeconfigs.NodeRoleNode:
return fluentBitRoleNode, nil
case nodeconfigs.NodeRoleDNS:
return fluentBitRoleDNS, nil
default:
return "", fmt.Errorf("unsupported fluent-bit role '%s'", role)
}
}
func normalizeRoles(rawRoles []string) []string {
roleSet := map[string]struct{}{}
for _, role := range rawRoles {
role = strings.ToLower(strings.TrimSpace(role))
if role != fluentBitRoleNode && role != fluentBitRoleDNS {
continue
}
roleSet[role] = struct{}{}
}
roles := make([]string, 0, len(roleSet))
for role := range roleSet {
roles = append(roles, role)
}
sort.Strings(roles)
return roles
}
func hasRole(roles []string, role string) bool {
for _, one := range roles {
if one == role {
return true
}
}
return false
}
func (this *BaseInstaller) buildDesiredFluentBitConfig(roles []string) (*fluentBitDesiredConfig, error) {
if len(roles) == 0 {
return nil, errors.New("fluent-bit roles should not be empty")
}
ch, err := models.SharedSysSettingDAO.ReadClickHouseConfig(nil)
if err != nil {
return nil, fmt.Errorf("read clickhouse setting failed: %w", err)
}
if ch == nil {
ch = &systemconfigs.ClickHouseSetting{}
}
if strings.TrimSpace(ch.Host) == "" {
ch.Host = "127.0.0.1"
}
ch.Scheme = strings.ToLower(strings.TrimSpace(ch.Scheme))
if ch.Scheme == "" {
ch.Scheme = "https"
}
if ch.Scheme != "http" && ch.Scheme != "https" {
return nil, fmt.Errorf("unsupported clickhouse scheme '%s'", ch.Scheme)
}
if ch.Port <= 0 {
if ch.Scheme == "https" {
ch.Port = 8443
} else {
ch.Port = 8443
}
}
if strings.TrimSpace(ch.Database) == "" {
ch.Database = "default"
}
if strings.TrimSpace(ch.User) == "" {
ch.User = "default"
}
// 当前平台策略:后台固定跳过 ClickHouse TLS 证书校验,不暴露 ServerName 配置。
ch.TLSSkipVerify = true
ch.TLSServerName = ""
httpPathPattern := fluentBitHTTPPathPattern
dnsPathPattern := fluentBitDNSPathPattern
publicPolicyPath, err := this.readPublicAccessLogPolicyPath()
if err != nil {
return nil, err
}
policyDir := dirFromPolicyPath(publicPolicyPath)
if policyDir != "" {
pattern := strings.TrimRight(policyDir, "/") + "/*.log"
httpPathPattern = pattern
dnsPathPattern = pattern
}
return &fluentBitDesiredConfig{
Roles: normalizeRoles(roles),
ClickHouse: ch,
HTTPPathPattern: httpPathPattern,
DNSPathPattern: dnsPathPattern,
}, nil
}
func (this *BaseInstaller) readPublicAccessLogPolicyPath() (string, error) {
policyId, err := models.SharedHTTPAccessLogPolicyDAO.FindCurrentPublicPolicyId(nil)
if err != nil {
return "", fmt.Errorf("find current public access log policy failed: %w", err)
}
if policyId <= 0 {
return "", nil
}
policy, err := models.SharedHTTPAccessLogPolicyDAO.FindEnabledHTTPAccessLogPolicy(nil, policyId)
if err != nil {
return "", fmt.Errorf("read public access log policy failed: %w", err)
}
if policy == nil {
return "", nil
}
return strings.TrimSpace(models.ParseHTTPAccessLogPolicyFilePath(policy)), nil
}
func dirFromPolicyPath(policyPath string) string {
pathValue := strings.TrimSpace(policyPath)
if pathValue == "" {
return ""
}
pathValue = strings.ReplaceAll(pathValue, "\\", "/")
dir := slashpath.Dir(pathValue)
if dir == "." {
return ""
}
return strings.TrimRight(dir, "/")
}
func (this *BaseInstaller) applyManagedConfig(tempDir string, desired *fluentBitDesiredConfig, parserContent string, existingMeta *fluentBitManagedMeta) (bool, error) {
mainExists, err := this.remoteFileExists(fluentBitMainConfigFile)
if err != nil {
return false, err
}
if mainExists && existingMeta == nil {
containsMarker, err := this.remoteFileContains(fluentBitMainConfigFile, fluentBitManagedMarker)
if err != nil {
return false, err
}
if !containsMarker {
// Adopt unmanaged config by backing it up and replacing with managed config below.
}
}
configContent, err := renderManagedConfig(desired)
if err != nil {
return false, err
}
envContent := renderManagedEnv(desired.ClickHouse)
metaContent, newMeta, err := renderManagedMeta(desired, configContent, parserContent, envContent)
if err != nil {
return false, err
}
requiredFiles := []string{fluentBitMainConfigFile, fluentBitParsersFile, fluentBitManagedEnvFile, fluentBitManagedMetaFile}
if existingMeta != nil && existingMeta.Hash == newMeta.Hash {
allExists := true
for _, file := range requiredFiles {
exists, err := this.remoteFileExists(file)
if err != nil {
return false, err
}
if !exists {
allExists = false
break
}
}
if allExists {
return false, nil
}
}
if mainExists {
backup := fluentBitMainConfigFile + ".bak." + strconv.FormatInt(time.Now().Unix(), 10)
_, stderr, err := this.client.Exec("cp -f " + shQuote(fluentBitMainConfigFile) + " " + shQuote(backup))
if err != nil {
return false, fmt.Errorf("backup existing fluent-bit config failed: %w, stderr: %s", err, stderr)
}
}
if err := this.writeRemoteFileByTemp(tempDir, fluentBitMainConfigFile, configContent, 0644); err != nil {
return false, err
}
if err := this.writeRemoteFileByTemp(tempDir, fluentBitParsersFile, parserContent, 0644); err != nil {
return false, err
}
if err := this.writeRemoteFileByTemp(tempDir, fluentBitManagedEnvFile, envContent, 0600); err != nil {
return false, err
}
if err := this.writeRemoteFileByTemp(tempDir, fluentBitManagedMetaFile, metaContent, 0644); err != nil {
return false, err
}
return true, nil
}
func renderManagedConfig(desired *fluentBitDesiredConfig) (string, error) {
if desired == nil || desired.ClickHouse == nil {
return "", errors.New("invalid fluent-bit desired config")
}
scheme := strings.ToLower(strings.TrimSpace(desired.ClickHouse.Scheme))
if scheme == "" {
scheme = "http"
}
if scheme != "http" && scheme != "https" {
return "", fmt.Errorf("unsupported clickhouse scheme '%s'", desired.ClickHouse.Scheme)
}
useTLS := scheme == "https"
insertHTTP := url.QueryEscape(fmt.Sprintf("INSERT INTO %s.logs_ingest FORMAT JSONEachRow", desired.ClickHouse.Database))
insertDNS := url.QueryEscape(fmt.Sprintf("INSERT INTO %s.dns_logs_ingest FORMAT JSONEachRow", desired.ClickHouse.Database))
lines := []string{
"# " + fluentBitManagedMarker,
"[SERVICE]",
" Flush 1",
" Log_Level info",
" Parsers_File " + fluentBitParsersFile,
" storage.path " + fluentBitStorageDir,
" storage.sync normal",
" storage.checksum off",
" storage.backlog.mem_limit 512MB",
"",
}
if hasRole(desired.Roles, fluentBitRoleNode) {
lines = append(lines,
"[INPUT]",
" Name tail",
" Path "+desired.HTTPPathPattern,
" Tag app.http.logs",
" Parser json",
" Refresh_Interval 2",
" Read_from_Head false",
" DB /var/lib/fluent-bit/http-logs.db",
" storage.type filesystem",
" Mem_Buf_Limit 256MB",
" Skip_Long_Lines On",
"",
)
}
if hasRole(desired.Roles, fluentBitRoleDNS) {
lines = append(lines,
"[INPUT]",
" Name tail",
" Path "+desired.DNSPathPattern,
" Tag app.dns.logs",
" Parser json",
" Refresh_Interval 2",
" Read_from_Head false",
" DB /var/lib/fluent-bit/dns-logs.db",
" storage.type filesystem",
" Mem_Buf_Limit 256MB",
" Skip_Long_Lines On",
"",
)
}
if hasRole(desired.Roles, fluentBitRoleNode) {
lines = append(lines,
"[OUTPUT]",
" Name http",
" Match app.http.logs",
" Host "+desired.ClickHouse.Host,
" Port "+strconv.Itoa(desired.ClickHouse.Port),
" URI /?query="+insertHTTP,
" Format json_lines",
" http_user ${CH_USER}",
" http_passwd ${CH_PASSWORD}",
" json_date_key timestamp",
" json_date_format epoch",
" workers 2",
" net.keepalive On",
" Retry_Limit False",
)
if useTLS {
lines = append(lines, " tls On")
if desired.ClickHouse.TLSSkipVerify {
lines = append(lines, " tls.verify Off")
} else {
lines = append(lines, " tls.verify On")
}
if strings.TrimSpace(desired.ClickHouse.TLSServerName) != "" {
lines = append(lines, " tls.vhost "+strings.TrimSpace(desired.ClickHouse.TLSServerName))
}
}
lines = append(lines, "")
}
if hasRole(desired.Roles, fluentBitRoleDNS) {
lines = append(lines,
"[OUTPUT]",
" Name http",
" Match app.dns.logs",
" Host "+desired.ClickHouse.Host,
" Port "+strconv.Itoa(desired.ClickHouse.Port),
" URI /?query="+insertDNS,
" Format json_lines",
" http_user ${CH_USER}",
" http_passwd ${CH_PASSWORD}",
" json_date_key timestamp",
" json_date_format epoch",
" workers 2",
" net.keepalive On",
" Retry_Limit False",
)
if useTLS {
lines = append(lines, " tls On")
if desired.ClickHouse.TLSSkipVerify {
lines = append(lines, " tls.verify Off")
} else {
lines = append(lines, " tls.verify On")
}
if strings.TrimSpace(desired.ClickHouse.TLSServerName) != "" {
lines = append(lines, " tls.vhost "+strings.TrimSpace(desired.ClickHouse.TLSServerName))
}
}
lines = append(lines, "")
}
return strings.Join(lines, "\n"), nil
}
func renderManagedEnv(ch *systemconfigs.ClickHouseSetting) string {
user := "default"
password := ""
if ch != nil {
if strings.TrimSpace(ch.User) != "" {
user = strings.TrimSpace(ch.User)
}
password = ch.Password
}
return "CH_USER=" + strconv.Quote(user) + "\n" +
"CH_PASSWORD=" + strconv.Quote(password) + "\n"
}
func renderManagedMeta(desired *fluentBitDesiredConfig, configContent string, parserContent string, envContent string) (string, *fluentBitManagedMeta, error) {
hashInput := configContent + "\n---\n" + parserContent + "\n---\n" + envContent + "\n---\n" + strings.Join(desired.Roles, ",")
hashBytes := sha256.Sum256([]byte(hashInput))
hash := fmt.Sprintf("%x", hashBytes[:])
meta := &fluentBitManagedMeta{
Roles: desired.Roles,
Hash: hash,
UpdatedAt: time.Now().Unix(),
SourceVersion: teaconst.Version,
}
data, err := json.MarshalIndent(meta, "", " ")
if err != nil {
return "", nil, fmt.Errorf("encode fluent-bit managed metadata failed: %w", err)
}
return string(data) + "\n", meta, nil
}
func (this *BaseInstaller) copyLocalFileToRemote(tempDir string, localPath string, remotePath string, mode os.FileMode) error {
tempFile := tempDir + "/" + filepath.Base(remotePath)
if err := this.client.Copy(localPath, tempFile, mode); err != nil {
return fmt.Errorf("upload fluent-bit file '%s' failed: %w", localPath, err)
}
_, stderr, err := this.client.Exec("cp -f " + shQuote(tempFile) + " " + shQuote(remotePath) + " && chmod " + fmt.Sprintf("%04o", mode) + " " + shQuote(remotePath))
if err != nil {
return fmt.Errorf("install remote fluent-bit file '%s' failed: %w, stderr: %s", remotePath, err, stderr)
}
return nil
}
func (this *BaseInstaller) writeRemoteFileByTemp(tempDir string, remotePath string, content string, mode os.FileMode) error {
tempFile := tempDir + "/" + filepath.Base(remotePath) + ".tmp"
if _, err := this.client.WriteFile(tempFile, []byte(content)); err != nil {
return fmt.Errorf("write temp fluent-bit file '%s' failed: %w", tempFile, err)
}
_, stderr, err := this.client.Exec("cp -f " + shQuote(tempFile) + " " + shQuote(remotePath) + " && chmod " + fmt.Sprintf("%04o", mode) + " " + shQuote(remotePath))
if err != nil {
return fmt.Errorf("write managed fluent-bit file '%s' failed: %w, stderr: %s", remotePath, err, stderr)
}
return nil
}
func (this *BaseInstaller) ensureFluentBitService(tempDir string, binPath string, configChanged bool) error {
_, _, _ = this.client.Exec("if command -v systemctl >/dev/null 2>&1 && [ ! -f /etc/systemd/system/" + fluentBitServiceName + ".service ] && [ ! -f /lib/systemd/system/" + fluentBitServiceName + ".service ]; then " +
"cat > /etc/systemd/system/" + fluentBitServiceName + ".service <<'EOF'\n" +
"[Unit]\n" +
"Description=Fluent Bit\n" +
"After=network.target\n" +
"\n" +
"[Service]\n" +
"ExecStart=" + binPath + " -c " + fluentBitMainConfigFile + "\n" +
"Restart=always\n" +
"RestartSec=5\n" +
"\n" +
"[Install]\n" +
"WantedBy=multi-user.target\n" +
"EOF\n" +
"fi")
stdout, _, err := this.client.Exec("if command -v systemctl >/dev/null 2>&1; then echo 1; else echo 0; fi")
if err != nil {
return fmt.Errorf("check systemctl failed: %w", err)
}
if strings.TrimSpace(stdout) == "1" {
dropInChanged, err := this.ensureServiceDropIn(tempDir, binPath)
if err != nil {
return err
}
restartRequired := configChanged || dropInChanged
_, stderr, err := this.client.Exec("systemctl daemon-reload; systemctl enable " + fluentBitServiceName + " >/dev/null 2>&1 || true; " +
"if systemctl is-active " + fluentBitServiceName + " >/dev/null 2>&1; then " +
"if [ \"" + boolToString(restartRequired) + "\" = \"1\" ]; then systemctl restart " + fluentBitServiceName + "; fi; " +
"else systemctl start " + fluentBitServiceName + "; fi")
if err != nil {
return fmt.Errorf("ensure fluent-bit service failed: %w, stderr: %s", err, stderr)
}
return nil
}
if configChanged {
_, _, _ = this.client.Exec("pkill -f \"fluent-bit.*fluent-bit.conf\" >/dev/null 2>&1 || true")
}
_, _, runningErr := this.client.Exec("pgrep -f \"fluent-bit.*fluent-bit.conf\" >/dev/null 2>&1")
if runningErr != nil {
startCmd := "set -a; [ -f " + shQuote(fluentBitManagedEnvFile) + " ] && . " + shQuote(fluentBitManagedEnvFile) + "; set +a; " +
shQuote(binPath) + " -c " + shQuote(fluentBitMainConfigFile) + " >/dev/null 2>&1 &"
_, stderr, err := this.client.Exec(startCmd)
if err != nil {
return fmt.Errorf("start fluent-bit without systemd failed: %w, stderr: %s", err, stderr)
}
}
return nil
}
func (this *BaseInstaller) ensureServiceDropIn(tempDir string, binPath string) (bool, error) {
_, stderr, err := this.client.Exec("mkdir -p " + shQuote(fluentBitDropInDir))
if err != nil {
return false, fmt.Errorf("prepare fluent-bit drop-in dir failed: %w, stderr: %s", err, stderr)
}
content := "[Service]\n" +
"EnvironmentFile=-" + fluentBitManagedEnvFile + "\n" +
"ExecStart=\n" +
"ExecStart=" + binPath + " -c " + fluentBitMainConfigFile + "\n"
existing, _, _ := this.client.Exec("if [ -f " + shQuote(fluentBitDropInFile) + " ]; then cat " + shQuote(fluentBitDropInFile) + "; fi")
if existing == content {
return false, nil
}
if err := this.writeRemoteFileByTemp(tempDir, fluentBitDropInFile, content, 0644); err != nil {
return false, err
}
return true, nil
}
func (this *BaseInstaller) remoteFileExists(path string) (bool, error) {
stdout, stderr, err := this.client.Exec("if [ -f " + shQuote(path) + " ]; then echo 1; else echo 0; fi")
if err != nil {
return false, fmt.Errorf("check remote file '%s' failed: %w, stderr: %s", path, err, stderr)
}
return strings.TrimSpace(stdout) == "1", nil
}
func (this *BaseInstaller) remoteFileContains(path string, pattern string) (bool, error) {
stdout, stderr, err := this.client.Exec("if grep -F " + shQuote(pattern) + " " + shQuote(path) + " >/dev/null 2>&1; then echo 1; else echo 0; fi")
if err != nil {
return false, fmt.Errorf("check remote file content '%s' failed: %w, stderr: %s", path, err, stderr)
}
return strings.TrimSpace(stdout) == "1", nil
}
func shQuote(value string) string {
if value == "" {
return "''"
}
return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'"
}
func boolToString(v bool) string {
if v {
return "1"
}
return "0"
}

View File

@@ -279,3 +279,27 @@ func (this *BaseInstaller) uname() (uname string) {
return "x86_64 GNU/Linux"
}
func parseOSRelease(content string) map[string]string {
result := map[string]string{}
lines := strings.Split(content, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") || !strings.Contains(line, "=") {
continue
}
parts := strings.SplitN(line, "=", 2)
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
value = strings.Trim(value, "\"")
result[key] = value
}
return result
}
func shQuote(s string) string {
if len(s) == 0 {
return "''"
}
return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'"
}

View File

@@ -0,0 +1,231 @@
package installers
import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
)
type HTTPDNSNodeInstaller struct {
BaseInstaller
}
func (i *HTTPDNSNodeInstaller) Install(dir string, params interface{}, installStatus *models.NodeInstallStatus) error {
if params == nil {
return errors.New("'params' required for node installation")
}
nodeParams, ok := params.(*NodeParams)
if !ok {
return errors.New("'params' should be *NodeParams")
}
err := nodeParams.Validate()
if err != nil {
return fmt.Errorf("params validation: %w", err)
}
installRootDir, appDir := resolveHTTPDNSInstallPaths(dir)
_, err = i.client.Stat(installRootDir)
if err != nil {
err = i.client.MkdirAll(installRootDir)
if err != nil {
installStatus.ErrorCode = "CREATE_ROOT_DIRECTORY_FAILED"
return fmt.Errorf("create directory '%s' failed: %w", installRootDir, err)
}
}
env, err := i.InstallHelper(installRootDir, nodeconfigs.NodeRoleHTTPDNS)
if err != nil {
installStatus.ErrorCode = "INSTALL_HELPER_FAILED"
return err
}
filePrefix := "edge-httpdns-" + env.OS + "-" + env.Arch
zipFile, err := i.LookupLatestInstallerForTarget(filePrefix, env)
if err != nil {
return err
}
if len(zipFile) == 0 {
return errors.New("can not find installer file for " + env.OS + "/" + env.Arch + ", expected '" + filePrefix + "-v*.zip' or distro-specific '" + filePrefix + "-{ubuntu22.04|amzn2023}-v*.zip'")
}
targetZip, err := i.copyZipToRemote(installRootDir, zipFile)
if err != nil {
return err
}
if !nodeParams.IsUpgrading {
_, stderr, testErr := i.client.Exec(env.HelperPath + " -cmd=test")
if testErr != nil {
return fmt.Errorf("test failed: %w", testErr)
}
if len(stderr) > 0 {
return errors.New("test failed: " + stderr)
}
}
exePath := appDir + "/bin/edge-httpdns"
if nodeParams.IsUpgrading {
_, err = i.client.Stat(exePath)
if err == nil {
_, _, _ = i.client.Exec(exePath + " stop")
removeErr := i.client.Remove(exePath)
if removeErr != nil && removeErr != os.ErrNotExist {
return fmt.Errorf("remove old file failed: %w", removeErr)
}
}
}
_, stderr, err := i.client.Exec(env.HelperPath + " -cmd=unzip -zip=\"" + targetZip + "\" -target=\"" + installRootDir + "\"")
if err != nil {
return err
}
if len(stderr) > 0 {
return errors.New("unzip installer failed: " + stderr)
}
certFile := appDir + "/configs/tls/server.crt"
keyFile := appDir + "/configs/tls/server.key"
err = i.writeTLSCertificate(certFile, keyFile, nodeParams.TLSCertData, nodeParams.TLSKeyData)
if err != nil {
installStatus.ErrorCode = "WRITE_TLS_CERT_FAILED"
return err
}
configFile := appDir + "/configs/api_httpdns.yaml"
if i.client.sudo {
_, _, _ = i.client.Exec("chown " + i.client.User() + " " + filepath.Dir(configFile))
}
listenAddr := strings.TrimSpace(nodeParams.HTTPDNSListenAddr)
if len(listenAddr) == 0 {
listenAddr = ":443"
}
configData := []byte(`rpc.endpoints: [ ${endpoints} ]
nodeId: "${nodeId}"
secret: "${nodeSecret}"
https.listenAddr: "${listenAddr}"
https.cert: "${certFile}"
https.key: "${keyFile}"`)
certFileClean := strings.ReplaceAll(certFile, "\\", "/")
keyFileClean := strings.ReplaceAll(keyFile, "\\", "/")
configData = bytes.ReplaceAll(configData, []byte("${endpoints}"), []byte(nodeParams.QuoteEndpoints()))
configData = bytes.ReplaceAll(configData, []byte("${nodeId}"), []byte(nodeParams.NodeId))
configData = bytes.ReplaceAll(configData, []byte("${nodeSecret}"), []byte(nodeParams.Secret))
configData = bytes.ReplaceAll(configData, []byte("${listenAddr}"), []byte(listenAddr))
configData = bytes.ReplaceAll(configData, []byte("${certFile}"), []byte(certFileClean))
configData = bytes.ReplaceAll(configData, []byte("${keyFile}"), []byte(keyFileClean))
_, err = i.client.WriteFile(configFile, configData)
if err != nil {
return fmt.Errorf("write '%s': %w", configFile, err)
}
startCmdPrefix := "cd " + shQuote(appDir+"/configs") + " && ../bin/edge-httpdns "
stdout, stderr, err := i.client.Exec(startCmdPrefix + "test")
if err != nil {
installStatus.ErrorCode = "TEST_FAILED"
return fmt.Errorf("test edge-httpdns failed: %w, stdout: %s, stderr: %s", err, stdout, stderr)
}
if len(stderr) > 0 {
if regexp.MustCompile(`(?i)rpc`).MatchString(stderr) || regexp.MustCompile(`(?i)rpc`).MatchString(stdout) {
installStatus.ErrorCode = "RPC_TEST_FAILED"
}
return errors.New("test edge-httpdns failed, stdout: " + stdout + ", stderr: " + stderr)
}
stdout, stderr, err = i.client.Exec(startCmdPrefix + "start")
if err != nil {
return fmt.Errorf("start edge-httpdns failed: %w, stdout: %s, stderr: %s", err, stdout, stderr)
}
if len(stderr) > 0 {
return errors.New("start edge-httpdns failed, stdout: " + stdout + ", stderr: " + stderr)
}
return nil
}
func resolveHTTPDNSInstallPaths(rawDir string) (installRootDir string, appDir string) {
dir := strings.TrimSpace(rawDir)
dir = strings.TrimRight(dir, "/")
if len(dir) == 0 {
return rawDir, rawDir + "/edge-httpdns"
}
if strings.HasSuffix(dir, "/edge-httpdns") {
root := strings.TrimSuffix(dir, "/edge-httpdns")
if len(root) == 0 {
root = "/"
}
return root, dir
}
return dir, dir + "/edge-httpdns"
}
func (i *HTTPDNSNodeInstaller) copyZipToRemote(dir string, zipFile string) (string, error) {
targetZip := ""
var firstCopyErr error
zipName := filepath.Base(zipFile)
for _, candidate := range []string{
dir + "/" + zipName,
i.client.UserHome() + "/" + zipName,
"/tmp/" + zipName,
} {
err := i.client.Copy(zipFile, candidate, 0777)
if err != nil {
if firstCopyErr == nil {
firstCopyErr = err
}
continue
}
targetZip = candidate
firstCopyErr = nil
break
}
if firstCopyErr != nil {
return "", fmt.Errorf("upload httpdns file failed: %w", firstCopyErr)
}
return targetZip, nil
}
func (i *HTTPDNSNodeInstaller) writeTLSCertificate(certFile string, keyFile string, certData []byte, keyData []byte) error {
if len(certData) == 0 || len(keyData) == 0 {
return errors.New("cluster tls certificate is empty")
}
certDir := filepath.Dir(certFile)
_, stderr, err := i.client.Exec("mkdir -p " + shQuote(certDir))
if err != nil {
return fmt.Errorf("create tls directory failed: %w, stderr: %s", err, stderr)
}
if i.client.sudo {
_, _, _ = i.client.Exec("chown " + i.client.User() + " " + shQuote(certDir))
}
_, err = i.client.WriteFile(certFile, certData)
if err != nil {
return fmt.Errorf("write cert file failed: %w", err)
}
_, err = i.client.WriteFile(keyFile, keyData)
if err != nil {
return fmt.Errorf("write key file failed: %w", err)
}
_, stderr, err = i.client.Exec("chmod 0644 " + shQuote(certFile) + " && chmod 0600 " + shQuote(keyFile))
if err != nil {
return fmt.Errorf("chmod tls files failed: %w, stderr: %s", err, stderr)
}
return nil
}

View File

@@ -137,12 +137,6 @@ secret: "${nodeSecret}"`)
}
}
// 在线安装/更新 Fluent Bit与边缘节点安装流程联动
err = this.SetupFluentBit(nodeconfigs.NodeRoleNode)
if err != nil {
installStatus.ErrorCode = "SETUP_FLUENT_BIT_FAILED"
return fmt.Errorf("setup fluent-bit failed: %w", err)
}
// 测试
_, stderr, err = this.client.Exec(dir + "/edge-node/bin/edge-node test")

View File

@@ -139,12 +139,6 @@ secret: "${nodeSecret}"`)
}
}
// 在线安装/更新 Fluent Bit与 DNS 节点安装流程联动)
err = this.SetupFluentBit(nodeconfigs.NodeRoleDNS)
if err != nil {
installStatus.ErrorCode = "SETUP_FLUENT_BIT_FAILED"
return fmt.Errorf("setup fluent-bit failed: %w", err)
}
// 测试
_, stderr, err = this.client.Exec(dir + "/edge-dns/bin/edge-dns test")

View File

@@ -6,10 +6,13 @@ import (
)
type NodeParams struct {
Endpoints []string
NodeId string
Secret string
IsUpgrading bool // 是否为升级
Endpoints []string
NodeId string
Secret string
TLSCertData []byte
TLSKeyData []byte
HTTPDNSListenAddr string
IsUpgrading bool // 是否为升级
}
func (this *NodeParams) Validate() error {

View File

@@ -0,0 +1,416 @@
package installers
import (
"encoding/json"
"errors"
"fmt"
"net"
"strconv"
"strings"
"time"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/goman"
"github.com/TeaOSLab/EdgeAPI/internal/utils"
"github.com/TeaOSLab/EdgeAPI/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
"github.com/iwind/TeaGo/logs"
)
var sharedHTTPDNSNodeQueue = NewHTTPDNSNodeQueue()
type HTTPDNSNodeQueue struct{}
func NewHTTPDNSNodeQueue() *HTTPDNSNodeQueue {
return &HTTPDNSNodeQueue{}
}
func SharedHTTPDNSNodeQueue() *HTTPDNSNodeQueue {
return sharedHTTPDNSNodeQueue
}
// InstallNodeProcess 鍦ㄧ嚎瀹夎 HTTPDNS 鑺傜偣娴佺▼鎺у埗
func (q *HTTPDNSNodeQueue) InstallNodeProcess(nodeId int64, isUpgrading bool) error {
installStatus := models.NewNodeInstallStatus()
installStatus.IsRunning = true
installStatus.IsFinished = false
installStatus.IsOk = false
installStatus.Error = ""
installStatus.ErrorCode = ""
installStatus.UpdatedAt = time.Now().Unix()
err := models.SharedHTTPDNSNodeDAO.UpdateNodeInstallStatus(nil, nodeId, installStatus)
if err != nil {
return err
}
ticker := utils.NewTicker(3 * time.Second)
goman.New(func() {
for ticker.Wait() {
installStatus.UpdatedAt = time.Now().Unix()
updateErr := models.SharedHTTPDNSNodeDAO.UpdateNodeInstallStatus(nil, nodeId, installStatus)
if updateErr != nil {
logs.Println("[HTTPDNS_INSTALL]" + updateErr.Error())
}
}
})
defer ticker.Stop()
err = q.InstallNode(nodeId, installStatus, isUpgrading)
installStatus.IsRunning = false
installStatus.IsFinished = true
if err != nil {
installStatus.IsOk = false
installStatus.Error = err.Error()
} else {
installStatus.IsOk = true
}
installStatus.UpdatedAt = time.Now().Unix()
updateErr := models.SharedHTTPDNSNodeDAO.UpdateNodeInstallStatus(nil, nodeId, installStatus)
if updateErr != nil {
return updateErr
}
if installStatus.IsOk {
return models.SharedHTTPDNSNodeDAO.UpdateNodeIsInstalled(nil, nodeId, true)
}
return nil
}
// InstallNode 鍦ㄧ嚎瀹夎 HTTPDNS 鑺傜偣
func (q *HTTPDNSNodeQueue) InstallNode(nodeId int64, installStatus *models.NodeInstallStatus, isUpgrading bool) 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) + "'")
}
cluster, err := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(nil, int64(node.ClusterId))
if err != nil {
return err
}
if cluster == nil {
return errors.New("can not find cluster")
}
sshHost, sshPort, grantId, err := q.parseSSHInfo(node, installStatus)
if err != nil {
return err
}
grant, err := models.SharedNodeGrantDAO.FindEnabledNodeGrant(nil, grantId)
if err != nil {
return err
}
if grant == nil {
installStatus.ErrorCode = "EMPTY_GRANT"
return errors.New("can not find user grant with id '" + numberutils.FormatInt64(grantId) + "'")
}
apiNodes, err := models.SharedAPINodeDAO.FindAllEnabledAndOnAPINodes(nil)
if err != nil {
return err
}
if len(apiNodes) == 0 {
return errors.New("no available api nodes")
}
apiEndpoints := make([]string, 0, 8)
for _, apiNode := range apiNodes {
addrConfigs, decodeErr := apiNode.DecodeAccessAddrs()
if decodeErr != nil {
return fmt.Errorf("decode api node access addresses failed: %w", decodeErr)
}
for _, addrConfig := range addrConfigs {
apiEndpoints = append(apiEndpoints, addrConfig.FullAddresses()...)
}
}
if len(apiEndpoints) == 0 {
return errors.New("no available api endpoints")
}
tlsCertData, tlsKeyData, err := q.resolveClusterTLSCertPair(cluster)
if err != nil {
installStatus.ErrorCode = "EMPTY_TLS_CERT"
return err
}
httpdnsListenAddr, err := q.resolveClusterTLSListenAddr(cluster)
if err != nil {
installStatus.ErrorCode = "INVALID_TLS_LISTEN"
return err
}
params := &NodeParams{
Endpoints: apiEndpoints,
NodeId: node.UniqueId,
Secret: node.Secret,
TLSCertData: tlsCertData,
TLSKeyData: tlsKeyData,
HTTPDNSListenAddr: httpdnsListenAddr,
IsUpgrading: isUpgrading,
}
installer := &HTTPDNSNodeInstaller{}
err = installer.Login(&Credentials{
Host: sshHost,
Port: sshPort,
Username: grant.Username,
Password: grant.Password,
PrivateKey: grant.PrivateKey,
Passphrase: grant.Passphrase,
Method: grant.Method,
Sudo: grant.Su == 1,
})
if err != nil {
installStatus.ErrorCode = "SSH_LOGIN_FAILED"
return err
}
defer func() {
_ = installer.Close()
}()
installDir := node.InstallDir
if len(installDir) == 0 {
if cluster != nil && len(cluster.InstallDir) > 0 {
installDir = cluster.InstallDir
}
if len(installDir) == 0 {
installDir = installer.client.UserHome() + "/edge-httpdns"
}
}
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")
}
if len(cluster.TLSPolicy) == 0 {
return nil, nil, errors.New("cluster tls policy is empty")
}
tlsConfig := map[string]json.RawMessage{}
if err := json.Unmarshal(cluster.TLSPolicy, &tlsConfig); err != nil {
return nil, nil, fmt.Errorf("decode cluster tls policy failed: %w", err)
}
sslPolicyData := tlsConfig["sslPolicy"]
if len(sslPolicyData) == 0 {
// Compatible with old data where TLSPolicy stores SSLPolicy directly.
sslPolicyData = json.RawMessage(cluster.TLSPolicy)
}
sslPolicy := &sslconfigs.SSLPolicy{}
if err := json.Unmarshal(sslPolicyData, sslPolicy); err != nil {
return nil, nil, fmt.Errorf("decode ssl policy failed: %w", err)
}
for _, cert := range sslPolicy.Certs {
if cert == nil {
continue
}
if len(cert.CertData) > 0 && len(cert.KeyData) > 0 {
return cert.CertData, cert.KeyData, nil
}
}
for _, certRef := range sslPolicy.CertRefs {
if certRef == nil || certRef.CertId <= 0 {
continue
}
certConfig, err := models.SharedSSLCertDAO.ComposeCertConfig(nil, certRef.CertId, false, nil, nil)
if err != nil {
return nil, nil, fmt.Errorf("load ssl cert %d failed: %w", certRef.CertId, err)
}
if certConfig == nil {
continue
}
if len(certConfig.CertData) > 0 && len(certConfig.KeyData) > 0 {
return certConfig.CertData, certConfig.KeyData, nil
}
}
if sslPolicy.Id > 0 {
policyConfig, err := models.SharedSSLPolicyDAO.ComposePolicyConfig(nil, sslPolicy.Id, false, nil, nil)
if err != nil {
return nil, nil, fmt.Errorf("load ssl policy %d failed: %w", sslPolicy.Id, err)
}
if policyConfig != nil {
for _, cert := range policyConfig.Certs {
if cert == nil {
continue
}
if len(cert.CertData) > 0 && len(cert.KeyData) > 0 {
return cert.CertData, cert.KeyData, nil
}
}
}
}
return nil, nil, errors.New("cluster tls certificate is not configured")
}
func (q *HTTPDNSNodeQueue) resolveClusterTLSListenAddr(cluster *models.HTTPDNSCluster) (string, error) {
const defaultListenAddr = ":443"
if cluster == nil || len(cluster.TLSPolicy) == 0 {
return defaultListenAddr, nil
}
tlsConfig, err := serverconfigs.NewTLSProtocolConfigFromJSON(cluster.TLSPolicy)
if err != nil {
return "", fmt.Errorf("decode cluster tls listen failed: %w", err)
}
for _, listen := range tlsConfig.Listen {
if listen == nil {
continue
}
if err := listen.Init(); err != nil {
return "", fmt.Errorf("invalid cluster tls listen address '%s': %w", listen.PortRange, err)
}
if listen.MinPort <= 0 {
continue
}
host := strings.TrimSpace(listen.Host)
return net.JoinHostPort(host, strconv.Itoa(listen.MinPort)), nil
}
return defaultListenAddr, nil
}
func (q *HTTPDNSNodeQueue) parseSSHInfo(node *models.HTTPDNSNode, installStatus *models.NodeInstallStatus) (string, int, int64, error) {
if node == nil {
return "", 0, 0, errors.New("node should not be nil")
}
login, err := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(nil, nodeconfigs.NodeRoleHTTPDNS, int64(node.Id))
if err != nil {
return "", 0, 0, err
}
if login == nil {
installStatus.ErrorCode = "EMPTY_SSH"
return "", 0, 0, errors.New("ssh login not found for node '" + numberutils.FormatInt64(int64(node.Id)) + "'")
}
sshParams, err := login.DecodeSSHParams()
if err != nil {
installStatus.ErrorCode = "EMPTY_SSH"
return "", 0, 0, err
}
if len(sshParams.Host) == 0 {
installStatus.ErrorCode = "EMPTY_SSH_HOST"
return "", 0, 0, errors.New("ssh host should not be empty")
}
if sshParams.Port <= 0 {
sshParams.Port = 22
}
if sshParams.GrantId <= 0 {
installStatus.ErrorCode = "EMPTY_GRANT"
return "", 0, 0, errors.New("grant id should not be empty")
}
return sshParams.Host, sshParams.Port, sshParams.GrantId, nil
}

View 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)
}()
}

View File

@@ -144,6 +144,17 @@ func (this *APINode) Start() {
this.processTableNames()
dbs.NotifyReady()
// 自动确保 ClickHouse 日志表存在(不阻断主流程)
this.setProgress("CLICKHOUSE", "正在检查 ClickHouse 日志表")
logs.Println("[API_NODE]ensuring clickhouse tables ...")
err = setup.EnsureClickHouseTables()
if err != nil {
logs.Println("[API_NODE]WARNING: ensure clickhouse tables failed: " + err.Error())
remotelogs.Error("API_NODE", "ensure clickhouse tables failed: "+err.Error())
} else {
logs.Println("[API_NODE]ensure clickhouse tables done")
}
// 设置时区
this.setProgress("TIMEZONE", "正在设置时区")
this.setupTimeZone()

View File

@@ -5,6 +5,7 @@ package nodes
import (
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services/clients"
httpdnsservices "github.com/TeaOSLab/EdgeAPI/internal/rpc/services/httpdns"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services/users"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"google.golang.org/grpc"
@@ -425,6 +426,46 @@ func (this *APINode) registerServices(server *grpc.Server) {
pb.RegisterDNSTaskServiceServer(server, instance)
this.rest(instance)
}
{
var instance = this.serviceInstance(&httpdnsservices.HTTPDNSClusterService{}).(*httpdnsservices.HTTPDNSClusterService)
pb.RegisterHTTPDNSClusterServiceServer(server, instance)
this.rest(instance)
}
{
var instance = this.serviceInstance(&httpdnsservices.HTTPDNSNodeService{}).(*httpdnsservices.HTTPDNSNodeService)
pb.RegisterHTTPDNSNodeServiceServer(server, instance)
this.rest(instance)
}
{
var instance = this.serviceInstance(&httpdnsservices.HTTPDNSAppService{}).(*httpdnsservices.HTTPDNSAppService)
pb.RegisterHTTPDNSAppServiceServer(server, instance)
this.rest(instance)
}
{
var instance = this.serviceInstance(&httpdnsservices.HTTPDNSDomainService{}).(*httpdnsservices.HTTPDNSDomainService)
pb.RegisterHTTPDNSDomainServiceServer(server, instance)
this.rest(instance)
}
{
var instance = this.serviceInstance(&httpdnsservices.HTTPDNSRuleService{}).(*httpdnsservices.HTTPDNSRuleService)
pb.RegisterHTTPDNSRuleServiceServer(server, instance)
this.rest(instance)
}
{
var instance = this.serviceInstance(&httpdnsservices.HTTPDNSAccessLogService{}).(*httpdnsservices.HTTPDNSAccessLogService)
pb.RegisterHTTPDNSAccessLogServiceServer(server, instance)
this.rest(instance)
}
{
var instance = this.serviceInstance(&httpdnsservices.HTTPDNSRuntimeLogService{}).(*httpdnsservices.HTTPDNSRuntimeLogService)
pb.RegisterHTTPDNSRuntimeLogServiceServer(server, instance)
this.rest(instance)
}
{
var instance = this.serviceInstance(&httpdnsservices.HTTPDNSSandboxService{}).(*httpdnsservices.HTTPDNSSandboxService)
pb.RegisterHTTPDNSSandboxServiceServer(server, instance)
this.rest(instance)
}
{
var instance = this.serviceInstance(&services.NodeClusterFirewallActionService{}).(*services.NodeClusterFirewallActionService)
pb.RegisterNodeClusterFirewallActionServiceServer(server, instance)

View File

@@ -7,6 +7,7 @@ import (
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services/accounts"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services/anti-ddos"
httpdnsServices "github.com/TeaOSLab/EdgeAPI/internal/rpc/services/httpdns"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services/nameservers"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services/posts"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services/reporters"
@@ -313,4 +314,9 @@ func APINodeServicesRegister(node *APINode, server *grpc.Server) {
pb.RegisterPostCategoryServiceServer(server, instance)
node.rest(instance)
}
{
var instance = node.serviceInstance(&httpdnsServices.HTTPDNSBoardService{}).(*httpdnsServices.HTTPDNSBoardService)
pb.RegisterHTTPDNSBoardServiceServer(server, instance)
node.rest(instance)
}
}

View File

@@ -0,0 +1,220 @@
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 {
if cluster == nil {
return nil
}
return &pb.HTTPDNSCluster{
Id: int64(cluster.Id),
IsOn: cluster.IsOn > 0,
IsDefault: cluster.IsDefault > 0,
Name: cluster.Name,
ServiceDomain: cluster.ServiceDomain,
DefaultTTL: cluster.DefaultTTL,
FallbackTimeoutMs: cluster.FallbackTimeoutMs,
InstallDir: cluster.InstallDir,
TlsPolicyJSON: cluster.TLSPolicy,
CreatedAt: int64(cluster.CreatedAt),
UpdatedAt: int64(cluster.UpdatedAt),
AutoRemoteStart: cluster.AutoRemoteStart > 0,
AccessLogIsOn: cluster.AccessLogIsOn > 0,
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
}
return &pb.HTTPDNSNode{
Id: int64(node.Id),
ClusterId: int64(node.ClusterId),
Name: node.Name,
IsOn: node.IsOn,
IsUp: node.IsUp,
IsInstalled: node.IsInstalled,
IsActive: node.IsActive,
UniqueId: node.UniqueId,
Secret: node.Secret,
InstallDir: node.InstallDir,
StatusJSON: node.Status,
InstallStatusJSON: node.InstallStatus,
CreatedAt: int64(node.CreatedAt),
UpdatedAt: int64(node.UpdatedAt),
}
}
func toPBApp(app *models.HTTPDNSApp, secret *models.HTTPDNSAppSecret) *pb.HTTPDNSApp {
if app == nil {
return nil
}
var signEnabled bool
var signSecret string
var signUpdatedAt int64
if secret != nil {
signEnabled = secret.SignEnabled
signSecret = secret.SignSecret
signUpdatedAt = int64(secret.SignUpdatedAt)
}
// 构建 clusterIdsJSON
clusterIds := models.SharedHTTPDNSAppDAO.ReadAppClusterIds(app)
clusterIdsJSON, _ := json.Marshal(clusterIds)
return &pb.HTTPDNSApp{
Id: int64(app.Id),
Name: app.Name,
AppId: app.AppId,
IsOn: app.IsOn,
SniMode: app.SNIMode,
SignEnabled: signEnabled,
SignSecret: signSecret,
SignUpdatedAt: signUpdatedAt,
CreatedAt: int64(app.CreatedAt),
UpdatedAt: int64(app.UpdatedAt),
UserId: int64(app.UserId),
ClusterIdsJSON: clusterIdsJSON,
}
}
func toPBDomain(domain *models.HTTPDNSDomain, ruleCount int64) *pb.HTTPDNSDomain {
if domain == nil {
return nil
}
return &pb.HTTPDNSDomain{
Id: int64(domain.Id),
AppId: int64(domain.AppId),
Domain: domain.Domain,
IsOn: domain.IsOn,
CreatedAt: int64(domain.CreatedAt),
UpdatedAt: int64(domain.UpdatedAt),
RuleCount: ruleCount,
}
}
func toPBRule(rule *models.HTTPDNSCustomRule, records []*models.HTTPDNSCustomRuleRecord) *pb.HTTPDNSCustomRule {
if rule == nil {
return nil
}
var pbRecords []*pb.HTTPDNSRuleRecord
for _, record := range records {
pbRecords = append(pbRecords, &pb.HTTPDNSRuleRecord{
Id: int64(record.Id),
RuleId: int64(record.RuleId),
RecordType: record.RecordType,
RecordValue: record.RecordValue,
Weight: record.Weight,
Sort: record.Sort,
})
}
return &pb.HTTPDNSCustomRule{
Id: int64(rule.Id),
AppId: int64(rule.AppId),
DomainId: int64(rule.DomainId),
RuleName: rule.RuleName,
LineScope: rule.LineScope,
LineCarrier: rule.LineCarrier,
LineRegion: rule.LineRegion,
LineProvince: rule.LineProvince,
LineContinent: rule.LineContinent,
LineCountry: rule.LineCountry,
Ttl: rule.TTL,
IsOn: rule.IsOn,
Priority: rule.Priority,
UpdatedAt: int64(rule.UpdatedAt),
Records: pbRecords,
}
}

View File

@@ -0,0 +1,287 @@
package httpdns
import (
"context"
"log"
"strconv"
"strings"
"time"
"github.com/TeaOSLab/EdgeAPI/internal/clickhouse"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
timeutil "github.com/iwind/TeaGo/utils/time"
)
type HTTPDNSAccessLogService struct {
services.BaseService
pb.UnimplementedHTTPDNSAccessLogServiceServer
}
func (s *HTTPDNSAccessLogService) CreateHTTPDNSAccessLogs(ctx context.Context, req *pb.CreateHTTPDNSAccessLogsRequest) (*pb.CreateHTTPDNSAccessLogsResponse, error) {
nodeIdInContext, err := s.ValidateHTTPDNSNode(ctx)
if err != nil {
_, err = s.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
}
if len(req.GetLogs()) == 0 {
return &pb.CreateHTTPDNSAccessLogsResponse{}, nil
}
mysqlLogs := make([]*models.HTTPDNSAccessLog, 0, len(req.GetLogs()))
chLogs := make([]*pb.HTTPDNSAccessLog, 0, len(req.GetLogs()))
seen := map[string]struct{}{}
for _, item := range req.GetLogs() {
if item == nil {
continue
}
nodeId := item.GetNodeId()
// When called by HTTPDNS node, trust node id parsed from RPC token.
if nodeIdInContext > 0 {
nodeId = nodeIdInContext
}
clusterId := item.GetClusterId()
if clusterId <= 0 && nodeId > 0 {
clusterId, _ = models.SharedHTTPDNSNodeDAO.FindNodeClusterId(s.NullTx(), nodeId)
}
key := item.GetRequestId() + "#" + strconv.FormatInt(nodeId, 10)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
createdAt := item.GetCreatedAt()
if createdAt <= 0 {
createdAt = time.Now().Unix()
}
day := item.GetDay()
if len(day) == 0 {
day = timeutil.Format("Ymd")
}
mysqlLogs = append(mysqlLogs, &models.HTTPDNSAccessLog{
RequestId: item.GetRequestId(),
ClusterId: uint32(clusterId),
NodeId: uint32(nodeId),
AppId: item.GetAppId(),
AppName: item.GetAppName(),
Domain: item.GetDomain(),
QType: item.GetQtype(),
ClientIP: item.GetClientIP(),
ClientRegion: item.GetClientRegion(),
Carrier: item.GetCarrier(),
SDKVersion: item.GetSdkVersion(),
OS: item.GetOs(),
ResultIPs: item.GetResultIPs(),
Status: item.GetStatus(),
ErrorCode: item.GetErrorCode(),
CostMs: item.GetCostMs(),
CreatedAt: uint64(createdAt),
Day: day,
Summary: item.GetSummary(),
})
chLogs = append(chLogs, &pb.HTTPDNSAccessLog{
RequestId: item.GetRequestId(),
ClusterId: clusterId,
NodeId: nodeId,
AppId: item.GetAppId(),
AppName: item.GetAppName(),
Domain: item.GetDomain(),
Qtype: item.GetQtype(),
ClientIP: item.GetClientIP(),
ClientRegion: item.GetClientRegion(),
Carrier: item.GetCarrier(),
SdkVersion: item.GetSdkVersion(),
Os: item.GetOs(),
ResultIPs: item.GetResultIPs(),
Status: item.GetStatus(),
ErrorCode: item.GetErrorCode(),
CostMs: item.GetCostMs(),
CreatedAt: createdAt,
Day: day,
Summary: item.GetSummary(),
})
}
if s.canWriteHTTPDNSAccessLogsToMySQL() {
for _, item := range mysqlLogs {
err = models.SharedHTTPDNSAccessLogDAO.CreateLog(s.NullTx(), item)
if err != nil {
if models.CheckSQLDuplicateErr(err) {
continue
}
return nil, err
}
}
}
store := clickhouse.NewHTTPDNSAccessLogsStore()
if s.canWriteHTTPDNSAccessLogsToClickHouse() && store.Client().IsConfigured() && len(chLogs) > 0 {
err = store.Insert(ctx, chLogs)
if err != nil {
log.Println("[HTTPDNS_ACCESS_LOG]write clickhouse failed, keep mysql success:", err.Error())
}
}
return &pb.CreateHTTPDNSAccessLogsResponse{}, nil
}
func (s *HTTPDNSAccessLogService) ListHTTPDNSAccessLogs(ctx context.Context, req *pb.ListHTTPDNSAccessLogsRequest) (*pb.ListHTTPDNSAccessLogsResponse, error) {
_, userId, err := s.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
allowedAppIds := []string(nil)
if userId > 0 {
if len(strings.TrimSpace(req.GetAppId())) > 0 {
app, err := ensureAppAccessByAppId(s.NullTx(), req.GetAppId(), userId)
if err != nil {
return nil, err
}
if app == nil {
return &pb.ListHTTPDNSAccessLogsResponse{
Logs: []*pb.HTTPDNSAccessLog{},
Total: 0,
}, nil
}
} else {
allowedAppIds, err = models.SharedHTTPDNSAppDAO.ListEnabledAppIdsWithUser(s.NullTx(), userId)
if err != nil {
return nil, err
}
if len(allowedAppIds) == 0 {
return &pb.ListHTTPDNSAccessLogsResponse{
Logs: []*pb.HTTPDNSAccessLog{},
Total: 0,
}, nil
}
}
}
store := clickhouse.NewHTTPDNSAccessLogsStore()
canReadFromClickHouse := s.shouldReadHTTPDNSAccessLogsFromClickHouse() && store.Client().IsConfigured()
canReadFromMySQL := s.shouldReadHTTPDNSAccessLogsFromMySQL()
if canReadFromClickHouse {
resp, listErr := s.listFromClickHouse(ctx, store, req, allowedAppIds)
if listErr == nil {
return resp, nil
}
log.Println("[HTTPDNS_ACCESS_LOG]read clickhouse failed, fallback mysql:", listErr.Error())
if !canReadFromMySQL {
return nil, listErr
}
}
if !canReadFromMySQL {
return &pb.ListHTTPDNSAccessLogsResponse{
Logs: []*pb.HTTPDNSAccessLog{},
Total: 0,
}, nil
}
total, err := models.SharedHTTPDNSAccessLogDAO.CountLogsWithAppIds(s.NullTx(), req.GetDay(), req.GetClusterId(), req.GetNodeId(), req.GetAppId(), allowedAppIds, req.GetDomain(), req.GetStatus(), req.GetKeyword())
if err != nil {
return nil, err
}
logs, err := models.SharedHTTPDNSAccessLogDAO.ListLogsWithAppIds(s.NullTx(), req.GetDay(), req.GetClusterId(), req.GetNodeId(), req.GetAppId(), allowedAppIds, req.GetDomain(), req.GetStatus(), req.GetKeyword(), req.GetOffset(), req.GetSize())
if err != nil {
return nil, err
}
result := make([]*pb.HTTPDNSAccessLog, 0, len(logs))
for _, item := range logs {
if item == nil {
continue
}
clusterName, _ := models.SharedHTTPDNSClusterDAO.FindEnabledClusterName(s.NullTx(), int64(item.ClusterId))
nodeName := ""
node, _ := models.SharedHTTPDNSNodeDAO.FindEnabledNode(s.NullTx(), int64(item.NodeId))
if node != nil {
nodeName = node.Name
}
result = append(result, &pb.HTTPDNSAccessLog{
Id: int64(item.Id),
RequestId: item.RequestId,
ClusterId: int64(item.ClusterId),
NodeId: int64(item.NodeId),
AppId: item.AppId,
AppName: item.AppName,
Domain: item.Domain,
Qtype: item.QType,
ClientIP: item.ClientIP,
ClientRegion: item.ClientRegion,
Carrier: item.Carrier,
SdkVersion: item.SDKVersion,
Os: item.OS,
ResultIPs: item.ResultIPs,
Status: item.Status,
ErrorCode: item.ErrorCode,
CostMs: item.CostMs,
CreatedAt: int64(item.CreatedAt),
Day: item.Day,
Summary: item.Summary,
NodeName: nodeName,
ClusterName: clusterName,
})
}
return &pb.ListHTTPDNSAccessLogsResponse{
Logs: result,
Total: total,
}, nil
}
func (s *HTTPDNSAccessLogService) listFromClickHouse(ctx context.Context, store *clickhouse.HTTPDNSAccessLogsStore, req *pb.ListHTTPDNSAccessLogsRequest, allowedAppIds []string) (*pb.ListHTTPDNSAccessLogsResponse, error) {
filter := clickhouse.HTTPDNSAccessLogListFilter{
Day: req.GetDay(),
ClusterId: req.GetClusterId(),
NodeId: req.GetNodeId(),
AppId: req.GetAppId(),
AppIds: allowedAppIds,
Domain: req.GetDomain(),
Status: req.GetStatus(),
Keyword: req.GetKeyword(),
Offset: req.GetOffset(),
Size: req.GetSize(),
}
total, err := store.Count(ctx, filter)
if err != nil {
return nil, err
}
rows, err := store.List(ctx, filter)
if err != nil {
return nil, err
}
result := make([]*pb.HTTPDNSAccessLog, 0, len(rows))
for _, row := range rows {
item := clickhouse.HTTPDNSRowToPB(row)
if item == nil {
continue
}
clusterName, _ := models.SharedHTTPDNSClusterDAO.FindEnabledClusterName(s.NullTx(), item.GetClusterId())
nodeName := ""
node, _ := models.SharedHTTPDNSNodeDAO.FindEnabledNode(s.NullTx(), item.GetNodeId())
if node != nil {
nodeName = node.Name
}
item.ClusterName = clusterName
item.NodeName = nodeName
result = append(result, item)
}
return &pb.ListHTTPDNSAccessLogsResponse{
Logs: result,
Total: total,
}, nil
}

View File

@@ -0,0 +1,19 @@
//go:build !plus
package httpdns
func (s *HTTPDNSAccessLogService) canWriteHTTPDNSAccessLogsToMySQL() bool {
return true
}
func (s *HTTPDNSAccessLogService) canWriteHTTPDNSAccessLogsToClickHouse() bool {
return true
}
func (s *HTTPDNSAccessLogService) shouldReadHTTPDNSAccessLogsFromClickHouse() bool {
return true
}
func (s *HTTPDNSAccessLogService) shouldReadHTTPDNSAccessLogsFromMySQL() bool {
return true
}

View File

@@ -0,0 +1,124 @@
//go:build plus
package httpdns
import (
"log"
"sync"
"time"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
)
var (
httpDNSAccessLogWriteTargetsLocker sync.RWMutex
httpDNSAccessLogWriteTargetsCache = &serverconfigs.AccessLogWriteTargets{
File: true,
MySQL: true,
ClickHouse: false,
}
httpDNSAccessLogWriteTargetsExpireAt int64
)
const httpDNSAccessLogWriteTargetsCacheTTL = 10 * time.Second
func (s *HTTPDNSAccessLogService) canWriteHTTPDNSAccessLogsToMySQL() bool {
targets := s.readHTTPDNSAccessLogWriteTargets()
if targets == nil {
return true
}
return targets.MySQL
}
func (s *HTTPDNSAccessLogService) canWriteHTTPDNSAccessLogsToClickHouse() bool {
targets := s.readHTTPDNSAccessLogWriteTargets()
if targets == nil {
return false
}
return targets.ClickHouse
}
func (s *HTTPDNSAccessLogService) shouldReadHTTPDNSAccessLogsFromClickHouse() bool {
targets := s.readHTTPDNSAccessLogWriteTargets()
if targets == nil {
return false
}
return targets.ClickHouse
}
func (s *HTTPDNSAccessLogService) shouldReadHTTPDNSAccessLogsFromMySQL() bool {
targets := s.readHTTPDNSAccessLogWriteTargets()
if targets == nil {
return true
}
return targets.MySQL
}
func (s *HTTPDNSAccessLogService) readHTTPDNSAccessLogWriteTargets() *serverconfigs.AccessLogWriteTargets {
now := time.Now().Unix()
httpDNSAccessLogWriteTargetsLocker.RLock()
if now < httpDNSAccessLogWriteTargetsExpireAt && httpDNSAccessLogWriteTargetsCache != nil {
targets := *httpDNSAccessLogWriteTargetsCache
httpDNSAccessLogWriteTargetsLocker.RUnlock()
return &targets
}
httpDNSAccessLogWriteTargetsLocker.RUnlock()
httpDNSAccessLogWriteTargetsLocker.Lock()
defer httpDNSAccessLogWriteTargetsLocker.Unlock()
// double-check
now = time.Now().Unix()
if now < httpDNSAccessLogWriteTargetsExpireAt && httpDNSAccessLogWriteTargetsCache != nil {
targets := *httpDNSAccessLogWriteTargetsCache
return &targets
}
targets := s.loadHTTPDNSAccessLogWriteTargetsFromPolicy()
if targets == nil {
targets = &serverconfigs.AccessLogWriteTargets{
File: true,
MySQL: true,
ClickHouse: false,
}
}
httpDNSAccessLogWriteTargetsCache = targets
httpDNSAccessLogWriteTargetsExpireAt = time.Now().Add(httpDNSAccessLogWriteTargetsCacheTTL).Unix()
copyTargets := *targets
return &copyTargets
}
func (s *HTTPDNSAccessLogService) loadHTTPDNSAccessLogWriteTargetsFromPolicy() *serverconfigs.AccessLogWriteTargets {
tx := s.NullTx()
publicPolicyId, err := models.SharedHTTPAccessLogPolicyDAO.FindCurrentPublicPolicyId(tx)
if err != nil {
log.Println("[HTTPDNS_ACCESS_LOG]load public access log policy failed:", err.Error())
return nil
}
if publicPolicyId <= 0 {
return &serverconfigs.AccessLogWriteTargets{
File: true,
MySQL: true,
ClickHouse: false,
}
}
policy, err := models.SharedHTTPAccessLogPolicyDAO.FindEnabledHTTPAccessLogPolicy(tx, publicPolicyId)
if err != nil {
log.Println("[HTTPDNS_ACCESS_LOG]load access log policy detail failed:", err.Error())
return nil
}
if policy == nil {
return &serverconfigs.AccessLogWriteTargets{
File: true,
MySQL: true,
ClickHouse: false,
}
}
return serverconfigs.ParseWriteTargetsFromPolicy(policy.WriteTargets, policy.Type, policy.DisableDefaultDB)
}

View File

@@ -0,0 +1,370 @@
package httpdns
import (
"context"
"encoding/json"
"errors"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/userconfigs"
"github.com/iwind/TeaGo/dbs"
"strings"
"time"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
)
// HTTPDNSAppService HTTPDNS应用服务
type HTTPDNSAppService struct {
services.BaseService
pb.UnimplementedHTTPDNSAppServiceServer
}
func (this *HTTPDNSAppService) CreateHTTPDNSApp(ctx context.Context, req *pb.CreateHTTPDNSAppRequest) (*pb.CreateHTTPDNSAppResponse, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
if userId > 0 {
req.UserId = userId
}
appName := strings.TrimSpace(req.Name)
appId := strings.TrimSpace(req.AppId)
if len(appName) == 0 || len(appId) == 0 {
return nil, errors.New("required 'name' and 'appId'")
}
var appDbId int64
now := time.Now().Unix()
err = this.RunTx(func(tx *dbs.Tx) error {
// 用户端防重复提交:短时间内同用户同应用名仅创建一次。
if req.UserId > 0 {
latest, err := models.SharedHTTPDNSAppDAO.FindLatestEnabledAppWithNameAndUser(tx, appName, req.UserId)
if err != nil {
return err
}
if latest != nil && int64(latest.CreatedAt) >= now-5 {
appDbId = int64(latest.Id)
secret, err := models.SharedHTTPDNSAppSecretDAO.FindEnabledAppSecret(tx, appDbId)
if err != nil {
return err
}
if secret == nil {
_, _, err = models.SharedHTTPDNSAppSecretDAO.InitAppSecret(tx, appDbId, req.SignEnabled)
if err != nil {
return err
}
}
return nil
}
}
exists, err := models.SharedHTTPDNSAppDAO.FindEnabledAppWithAppId(tx, appId)
if err != nil {
return err
}
if exists != nil {
return errors.New("appId already exists")
}
// 使用 clusterIdsJSON若为空则从用户关联集群获取
clusterIdsJSON := req.ClusterIdsJSON
if len(clusterIdsJSON) == 0 || string(clusterIdsJSON) == "[]" || string(clusterIdsJSON) == "null" {
// 读取用户关联的 HTTPDNS 集群
if req.UserId > 0 {
user, userErr := models.SharedUserDAO.FindEnabledUser(tx, req.UserId, nil)
if userErr != nil {
return userErr
}
if user != nil && len(user.HttpdnsClusterIds) > 0 {
var userClusterIds []int64
if json.Unmarshal([]byte(user.HttpdnsClusterIds), &userClusterIds) == nil && len(userClusterIds) > 0 {
clusterIdsJSON, _ = json.Marshal(userClusterIds)
}
}
}
}
// 如果仍然没有集群,则不允许创建
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)
if err != nil {
return err
}
_, _, err = models.SharedHTTPDNSAppSecretDAO.InitAppSecret(tx, appDbId, req.SignEnabled)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, appDbId, models.HTTPDNSNodeTaskTypeAppChanged)
})
if err != nil {
return nil, err
}
return &pb.CreateHTTPDNSAppResponse{AppDbId: appDbId}, nil
}
// readHTTPDNSDefaultClusterIdList reads default cluster IDs from UserRegisterConfig.
func readHTTPDNSDefaultClusterIdList(tx *dbs.Tx) ([]int64, error) {
// 优先从 UserRegisterConfig 中读取
configJSON, err := models.SharedSysSettingDAO.ReadSetting(tx, systemconfigs.SettingCodeUserRegisterConfig)
if err != nil {
return nil, err
}
if len(configJSON) > 0 {
var config userconfigs.UserRegisterConfig
if err := json.Unmarshal(configJSON, &config); err == nil {
if len(config.HTTPDNSDefaultClusterIds) > 0 {
// 验证集群有效性
var validIds []int64
for _, id := range config.HTTPDNSDefaultClusterIds {
if id <= 0 {
continue
}
cluster, err := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(tx, id)
if err != nil {
return nil, err
}
if cluster != nil && cluster.IsOn > 0 {
validIds = append(validIds, id)
}
}
if len(validIds) > 0 {
return validIds, nil
}
}
}
}
return nil, nil
}
func (this *HTTPDNSAppService) UpdateHTTPDNSApp(ctx context.Context, req *pb.UpdateHTTPDNSAppRequest) (*pb.RPCSuccess, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
oldApp, err := ensureAppAccess(tx, req.AppDbId, userId)
if err != nil {
return err
}
if oldApp == nil {
return errors.New("app not found")
}
targetUserId := req.UserId
if targetUserId <= 0 {
targetUserId = oldApp.UserId
}
if userId > 0 {
targetUserId = userId
}
err = models.SharedHTTPDNSAppDAO.UpdateApp(tx, req.AppDbId, req.Name, req.ClusterIdsJSON, req.IsOn, targetUserId)
if err != nil {
return err
}
err = notifyHTTPDNSAppTasksByApp(tx, oldApp, models.HTTPDNSNodeTaskTypeAppChanged)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, req.AppDbId, models.HTTPDNSNodeTaskTypeAppChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSAppService) DeleteHTTPDNSApp(ctx context.Context, req *pb.DeleteHTTPDNSAppRequest) (*pb.RPCSuccess, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
app, err := ensureAppAccess(tx, req.AppDbId, userId)
if err != nil {
return err
}
if app == nil {
return nil
}
// 1) 先停用规则记录
rules, err := models.SharedHTTPDNSCustomRuleDAO.ListEnabledRulesWithAppId(tx, req.AppDbId)
if err != nil {
return err
}
for _, rule := range rules {
err = models.SharedHTTPDNSCustomRuleRecordDAO.DisableRecordsWithRuleId(tx, int64(rule.Id))
if err != nil {
return err
}
}
// 2) 停用规则、域名、密钥
err = models.SharedHTTPDNSCustomRuleDAO.DisableRulesWithAppId(tx, req.AppDbId)
if err != nil {
return err
}
err = models.SharedHTTPDNSDomainDAO.DisableDomainsWithAppId(tx, req.AppDbId)
if err != nil {
return err
}
err = models.SharedHTTPDNSAppSecretDAO.DisableAppSecret(tx, req.AppDbId)
if err != nil {
return err
}
// 3) 删除该应用的 MySQL 访问日志,避免残留
err = models.SharedHTTPDNSAccessLogDAO.DeleteLogsWithAppId(tx, app.AppId)
if err != nil {
return err
}
// 4) 最后停用应用
err = models.SharedHTTPDNSAppDAO.DisableApp(tx, req.AppDbId)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByApp(tx, app, models.HTTPDNSNodeTaskTypeAppChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSAppService) FindHTTPDNSApp(ctx context.Context, req *pb.FindHTTPDNSAppRequest) (*pb.FindHTTPDNSAppResponse, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
app, err := ensureAppAccess(this.NullTx(), req.AppDbId, userId)
if err != nil {
return nil, err
}
if app == nil {
return &pb.FindHTTPDNSAppResponse{}, nil
}
secret, err := models.SharedHTTPDNSAppSecretDAO.FindEnabledAppSecret(this.NullTx(), req.AppDbId)
if err != nil {
return nil, err
}
return &pb.FindHTTPDNSAppResponse{App: toPBApp(app, secret)}, nil
}
func (this *HTTPDNSAppService) ListHTTPDNSApps(ctx context.Context, req *pb.ListHTTPDNSAppsRequest) (*pb.ListHTTPDNSAppsResponse, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
var apps []*models.HTTPDNSApp
if userId > 0 {
apps, err = models.SharedHTTPDNSAppDAO.ListEnabledAppsWithUser(this.NullTx(), userId, req.Offset, req.Size, req.Keyword)
} else {
apps, err = models.SharedHTTPDNSAppDAO.ListEnabledApps(this.NullTx(), req.Offset, req.Size, req.Keyword)
}
if err != nil {
return nil, err
}
var pbApps []*pb.HTTPDNSApp
for _, app := range apps {
secret, err := models.SharedHTTPDNSAppSecretDAO.FindEnabledAppSecret(this.NullTx(), int64(app.Id))
if err != nil {
return nil, err
}
pbApps = append(pbApps, toPBApp(app, secret))
}
return &pb.ListHTTPDNSAppsResponse{Apps: pbApps}, nil
}
func (this *HTTPDNSAppService) FindAllHTTPDNSApps(ctx context.Context, req *pb.FindAllHTTPDNSAppsRequest) (*pb.FindAllHTTPDNSAppsResponse, error) {
_, userId, validateErr := this.ValidateAdminAndUser(ctx, true)
if validateErr != nil {
if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
return nil, validateErr
}
}
var apps []*models.HTTPDNSApp
var err error
if validateErr == nil && userId > 0 {
apps, err = models.SharedHTTPDNSAppDAO.FindAllEnabledAppsWithUser(this.NullTx(), userId)
} else {
apps, err = models.SharedHTTPDNSAppDAO.FindAllEnabledApps(this.NullTx())
}
if err != nil {
return nil, err
}
var pbApps []*pb.HTTPDNSApp
for _, app := range apps {
secret, err := models.SharedHTTPDNSAppSecretDAO.FindEnabledAppSecret(this.NullTx(), int64(app.Id))
if err != nil {
return nil, err
}
pbApps = append(pbApps, toPBApp(app, secret))
}
return &pb.FindAllHTTPDNSAppsResponse{Apps: pbApps}, nil
}
func (this *HTTPDNSAppService) UpdateHTTPDNSAppSignEnabled(ctx context.Context, req *pb.UpdateHTTPDNSAppSignEnabledRequest) (*pb.RPCSuccess, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
app, err := ensureAppAccess(tx, req.AppDbId, userId)
if err != nil {
return err
}
if app == nil {
return errors.New("app not found")
}
err = models.SharedHTTPDNSAppSecretDAO.UpdateSignEnabled(tx, req.AppDbId, req.SignEnabled)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, req.AppDbId, models.HTTPDNSNodeTaskTypeAppChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSAppService) ResetHTTPDNSAppSignSecret(ctx context.Context, req *pb.ResetHTTPDNSAppSignSecretRequest) (*pb.ResetHTTPDNSAppSignSecretResponse, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
var signSecret string
var updatedAt int64
err = this.RunTx(func(tx *dbs.Tx) error {
app, err := ensureAppAccess(tx, req.AppDbId, userId)
if err != nil {
return err
}
if app == nil {
return errors.New("app not found")
}
signSecret, updatedAt, err = models.SharedHTTPDNSAppSecretDAO.ResetSignSecret(tx, req.AppDbId)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, req.AppDbId, models.HTTPDNSNodeTaskTypeAppChanged)
})
if err != nil {
return nil, err
}
return &pb.ResetHTTPDNSAppSignSecretResponse{
SignSecret: signSecret,
UpdatedAt: updatedAt,
}, nil
}

View File

@@ -0,0 +1,324 @@
package httpdns
import (
"context"
"time"
"github.com/TeaOSLab/EdgeAPI/internal/clickhouse"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeAPI/internal/utils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/dbs"
timeutil "github.com/iwind/TeaGo/utils/time"
)
// HTTPDNSBoardService HTTPDNS 仪表盘服务
type HTTPDNSBoardService struct {
services.BaseService
}
// ComposeHTTPDNSBoard 组合看板数据
func (this *HTTPDNSBoardService) ComposeHTTPDNSBoard(ctx context.Context, req *pb.ComposeHTTPDNSBoardRequest) (*pb.ComposeHTTPDNSBoardResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
tx := this.NullTx()
result := &pb.ComposeHTTPDNSBoardResponse{}
countApps, err := models.SharedHTTPDNSAppDAO.CountEnabledApps(tx, "")
if err != nil {
return nil, err
}
result.CountApps = countApps
countDomains, err := models.SharedHTTPDNSDomainDAO.CountEnabledDomains(tx, 0)
if err != nil {
return nil, err
}
result.CountDomains = countDomains
countClusters, err := models.SharedHTTPDNSClusterDAO.CountEnabledClusters(tx, "")
if err != nil {
return nil, err
}
result.CountClusters = countClusters
allNodes, err := models.SharedHTTPDNSNodeDAO.ListEnabledNodes(tx, 0)
if err != nil {
return nil, err
}
result.CountNodes = int64(len(allNodes))
var countOffline int64
for _, node := range allNodes {
if !node.IsActive {
countOffline++
}
}
result.CountOfflineNodes = countOffline
hourFrom := timeutil.Format("YmdH", time.Now().Add(-23*time.Hour))
hourTo := timeutil.Format("YmdH")
dayFrom := timeutil.Format("Ymd", time.Now().AddDate(0, 0, -14))
dayTo := timeutil.Format("Ymd")
todayFrom := timeutil.Format("Ymd", time.Now().Add(-24*time.Hour))
store := clickhouse.NewHTTPDNSAccessLogsStore()
if store.Client().IsConfigured() {
err = this.composeTrafficAndRanksFromClickHouse(ctx, tx, store, result, hourFrom, hourTo, dayFrom, dayTo, todayFrom)
}
if err != nil || !store.Client().IsConfigured() {
err = this.composeTrafficAndRanksFromMySQL(tx, result, hourFrom, hourTo, dayFrom, dayTo, todayFrom)
if err != nil {
return nil, err
}
}
err = this.fillNodeValues(tx, result)
if err != nil {
return nil, err
}
return result, nil
}
func (this *HTTPDNSBoardService) composeTrafficAndRanksFromClickHouse(ctx context.Context, tx *dbs.Tx, store *clickhouse.HTTPDNSAccessLogsStore, result *pb.ComposeHTTPDNSBoardResponse, hourFrom string, hourTo string, dayFrom string, dayTo string, todayFrom string) error {
hourlyStats, err := store.FindHourlyStats(ctx, hourFrom, hourTo)
if err != nil {
return err
}
hourlyMap := map[string]*clickhouse.HTTPDNSAccessLogHourlyStat{}
for _, stat := range hourlyStats {
hourlyMap[stat.Hour] = stat
}
hours, err := utils.RangeHours(hourFrom, hourTo)
if err != nil {
return err
}
for _, hour := range hours {
stat, ok := hourlyMap[hour]
if ok {
result.HourlyTrafficStats = append(result.HourlyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_HourlyTrafficStat{
Hour: hour,
CountRequests: stat.CountRequests,
})
} else {
result.HourlyTrafficStats = append(result.HourlyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_HourlyTrafficStat{Hour: hour})
}
}
dailyStats, err := store.FindDailyStats(ctx, dayFrom, dayTo)
if err != nil {
return err
}
dailyMap := map[string]*clickhouse.HTTPDNSAccessLogDailyStat{}
for _, stat := range dailyStats {
dailyMap[stat.Day] = stat
}
days, err := utils.RangeDays(dayFrom, dayTo)
if err != nil {
return err
}
for _, day := range days {
stat, ok := dailyMap[day]
if ok {
result.DailyTrafficStats = append(result.DailyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_DailyTrafficStat{
Day: day,
CountRequests: stat.CountRequests,
})
} else {
result.DailyTrafficStats = append(result.DailyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_DailyTrafficStat{Day: day})
}
}
topAppStats, err := store.ListTopApps(ctx, todayFrom, dayTo, 10)
if err != nil {
return err
}
for _, stat := range topAppStats {
appName := stat.AppName
if len(appName) == 0 {
appName = stat.AppId
}
result.TopAppStats = append(result.TopAppStats, &pb.ComposeHTTPDNSBoardResponse_TopAppStat{
AppId: 0,
AppName: appName,
CountRequests: stat.CountRequests,
})
}
topDomainStats, err := store.ListTopDomains(ctx, todayFrom, dayTo, 10)
if err != nil {
return err
}
for _, stat := range topDomainStats {
result.TopDomainStats = append(result.TopDomainStats, &pb.ComposeHTTPDNSBoardResponse_TopDomainStat{
DomainName: stat.Domain,
CountRequests: stat.CountRequests,
})
}
topNodeStats, err := store.ListTopNodes(ctx, todayFrom, dayTo, 10)
if err != nil {
return err
}
for _, stat := range topNodeStats {
nodeName := ""
node, nodeErr := models.SharedHTTPDNSNodeDAO.FindEnabledNode(tx, int64(stat.NodeId))
if nodeErr == nil && node != nil {
nodeName = node.Name
}
if len(nodeName) == 0 {
continue
}
result.TopNodeStats = append(result.TopNodeStats, &pb.ComposeHTTPDNSBoardResponse_TopNodeStat{
ClusterId: int64(stat.ClusterId),
NodeId: int64(stat.NodeId),
NodeName: nodeName,
CountRequests: stat.CountRequests,
})
}
return nil
}
func (this *HTTPDNSBoardService) composeTrafficAndRanksFromMySQL(tx *dbs.Tx, result *pb.ComposeHTTPDNSBoardResponse, hourFrom string, hourTo string, dayFrom string, dayTo string, todayFrom string) error {
hourlyStats, err := models.SharedHTTPDNSAccessLogDAO.FindHourlyStats(tx, hourFrom, hourTo)
if err != nil {
return err
}
hourlyMap := map[string]*models.HTTPDNSAccessLogHourlyStat{}
for _, stat := range hourlyStats {
hourlyMap[stat.Hour] = stat
}
hours, err := utils.RangeHours(hourFrom, hourTo)
if err != nil {
return err
}
for _, hour := range hours {
stat, ok := hourlyMap[hour]
if ok {
result.HourlyTrafficStats = append(result.HourlyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_HourlyTrafficStat{
Hour: hour,
CountRequests: stat.CountRequests,
})
} else {
result.HourlyTrafficStats = append(result.HourlyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_HourlyTrafficStat{Hour: hour})
}
}
dailyStats, err := models.SharedHTTPDNSAccessLogDAO.FindDailyStats(tx, dayFrom, dayTo)
if err != nil {
return err
}
dailyMap := map[string]*models.HTTPDNSAccessLogDailyStat{}
for _, stat := range dailyStats {
dailyMap[stat.Day] = stat
}
days, err := utils.RangeDays(dayFrom, dayTo)
if err != nil {
return err
}
for _, day := range days {
stat, ok := dailyMap[day]
if ok {
result.DailyTrafficStats = append(result.DailyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_DailyTrafficStat{
Day: day,
CountRequests: stat.CountRequests,
})
} else {
result.DailyTrafficStats = append(result.DailyTrafficStats, &pb.ComposeHTTPDNSBoardResponse_DailyTrafficStat{Day: day})
}
}
topAppStats, err := models.SharedHTTPDNSAccessLogDAO.ListTopApps(tx, todayFrom, dayTo, 10)
if err != nil {
return err
}
for _, stat := range topAppStats {
appName := stat.AppName
if len(appName) == 0 {
appName = stat.AppId
}
result.TopAppStats = append(result.TopAppStats, &pb.ComposeHTTPDNSBoardResponse_TopAppStat{
AppId: 0,
AppName: appName,
CountRequests: stat.CountRequests,
})
}
topDomainStats, err := models.SharedHTTPDNSAccessLogDAO.ListTopDomains(tx, todayFrom, dayTo, 10)
if err != nil {
return err
}
for _, stat := range topDomainStats {
result.TopDomainStats = append(result.TopDomainStats, &pb.ComposeHTTPDNSBoardResponse_TopDomainStat{
DomainName: stat.Domain,
CountRequests: stat.CountRequests,
})
}
topNodeStats, err := models.SharedHTTPDNSAccessLogDAO.ListTopNodes(tx, todayFrom, dayTo, 10)
if err != nil {
return err
}
for _, stat := range topNodeStats {
nodeName := ""
node, nodeErr := models.SharedHTTPDNSNodeDAO.FindEnabledNode(tx, int64(stat.NodeId))
if nodeErr == nil && node != nil {
nodeName = node.Name
}
if len(nodeName) == 0 {
continue
}
result.TopNodeStats = append(result.TopNodeStats, &pb.ComposeHTTPDNSBoardResponse_TopNodeStat{
ClusterId: int64(stat.ClusterId),
NodeId: int64(stat.NodeId),
NodeName: nodeName,
CountRequests: stat.CountRequests,
})
}
return nil
}
func (this *HTTPDNSBoardService) fillNodeValues(tx *dbs.Tx, result *pb.ComposeHTTPDNSBoardResponse) error {
cpuValues, err := models.SharedNodeValueDAO.ListValuesForHTTPDNSNodes(tx, nodeconfigs.NodeValueItemCPU, "usage", nodeconfigs.NodeValueRangeMinute)
if err != nil {
return err
}
for _, v := range cpuValues {
result.CpuNodeValues = append(result.CpuNodeValues, &pb.NodeValue{
ValueJSON: v.Value,
CreatedAt: int64(v.CreatedAt),
})
}
memoryValues, err := models.SharedNodeValueDAO.ListValuesForHTTPDNSNodes(tx, nodeconfigs.NodeValueItemMemory, "usage", nodeconfigs.NodeValueRangeMinute)
if err != nil {
return err
}
for _, v := range memoryValues {
result.MemoryNodeValues = append(result.MemoryNodeValues, &pb.NodeValue{
ValueJSON: v.Value,
CreatedAt: int64(v.CreatedAt),
})
}
loadValues, err := models.SharedNodeValueDAO.ListValuesForHTTPDNSNodes(tx, nodeconfigs.NodeValueItemLoad, "load1m", nodeconfigs.NodeValueRangeMinute)
if err != nil {
return err
}
for _, v := range loadValues {
result.LoadNodeValues = append(result.LoadNodeValues, &pb.NodeValue{
ValueJSON: v.Value,
CreatedAt: int64(v.CreatedAt),
})
}
return nil
}

View File

@@ -0,0 +1,216 @@
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集群服务
type HTTPDNSClusterService struct {
services.BaseService
pb.UnimplementedHTTPDNSClusterServiceServer
}
func (this *HTTPDNSClusterService) CreateHTTPDNSCluster(ctx context.Context, req *pb.CreateHTTPDNSClusterRequest) (*pb.CreateHTTPDNSClusterResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
if len(req.Name) == 0 {
return nil, errors.New("required 'name'")
}
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, req.TimeZone)
if err != nil {
return err
}
return notifyHTTPDNSClusterTask(tx, clusterId, models.HTTPDNSNodeTaskTypeConfigChanged)
})
if err != nil {
return nil, err
}
return &pb.CreateHTTPDNSClusterResponse{ClusterId: clusterId}, nil
}
func (this *HTTPDNSClusterService) UpdateHTTPDNSCluster(ctx context.Context, req *pb.UpdateHTTPDNSClusterRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
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 {
// 先读取旧的 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 && string(req.TlsPolicyJSON) != oldTLSJSON {
taskType = models.HTTPDNSNodeTaskTypeTLSChanged
}
return notifyHTTPDNSClusterTask(tx, req.ClusterId, taskType)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSClusterService) DeleteHTTPDNSCluster(ctx context.Context, req *pb.DeleteHTTPDNSClusterRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
err = models.SharedHTTPDNSClusterDAO.DisableCluster(tx, req.ClusterId)
if err != nil {
return err
}
return notifyHTTPDNSClusterTask(tx, req.ClusterId, models.HTTPDNSNodeTaskTypeConfigChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSClusterService) FindHTTPDNSCluster(ctx context.Context, req *pb.FindHTTPDNSClusterRequest) (*pb.FindHTTPDNSClusterResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
cluster, err := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(this.NullTx(), req.ClusterId)
if err != nil {
return nil, err
}
if cluster != nil {
_ = grpc.SetHeader(ctx, metadata.Pairs(
"x-httpdns-auto-remote-start", fmt.Sprintf("%t", cluster.AutoRemoteStart > 0),
"x-httpdns-access-log-is-on", fmt.Sprintf("%t", cluster.AccessLogIsOn > 0),
"x-httpdns-time-zone", cluster.TimeZone,
))
}
return &pb.FindHTTPDNSClusterResponse{Cluster: toPBCluster(cluster)}, nil
}
func (this *HTTPDNSClusterService) ListHTTPDNSClusters(ctx context.Context, req *pb.ListHTTPDNSClustersRequest) (*pb.ListHTTPDNSClustersResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
clusters, err := models.SharedHTTPDNSClusterDAO.ListEnabledClusters(this.NullTx(), req.Offset, req.Size, req.Keyword)
if err != nil {
return nil, err
}
var pbClusters []*pb.HTTPDNSCluster
for _, cluster := range clusters {
pbClusters = append(pbClusters, toPBCluster(cluster))
}
return &pb.ListHTTPDNSClustersResponse{Clusters: pbClusters}, nil
}
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 {
return nil, err
}
var pbClusters []*pb.HTTPDNSCluster
for _, cluster := range clusters {
if isNode {
// 节点调用时解析证书引用,嵌入实际 PEM 数据
pbClusters = append(pbClusters, toPBClusterWithResolvedCerts(this.NullTx(), cluster))
} else {
pbClusters = append(pbClusters, toPBCluster(cluster))
}
}
return &pb.FindAllHTTPDNSClustersResponse{Clusters: pbClusters}, nil
}
func (this *HTTPDNSClusterService) UpdateHTTPDNSClusterDefault(ctx context.Context, req *pb.UpdateHTTPDNSClusterDefaultRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
err = models.SharedHTTPDNSClusterDAO.UpdateDefaultCluster(tx, req.ClusterId)
if err != nil {
return err
}
clusters, err := models.SharedHTTPDNSClusterDAO.FindAllEnabledClusters(tx)
if err != nil {
return err
}
for _, cluster := range clusters {
err = notifyHTTPDNSClusterTask(tx, int64(cluster.Id), models.HTTPDNSNodeTaskTypeConfigChanged)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSClusterService) ListHTTPDNSNodesWithClusterId(ctx context.Context, req *pb.ListHTTPDNSNodesWithClusterIdRequest) (*pb.ListHTTPDNSNodesWithClusterIdResponse, error) {
_, _, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
nodes, err := models.SharedHTTPDNSNodeDAO.ListEnabledNodes(this.NullTx(), req.ClusterId)
if err != nil {
return nil, err
}
var pbNodes []*pb.HTTPDNSNode
for _, node := range nodes {
pbNodes = append(pbNodes, toPBNode(node))
}
return &pb.ListHTTPDNSNodesWithClusterIdResponse{Nodes: pbNodes}, nil
}

View File

@@ -0,0 +1,128 @@
package httpdns
import (
"context"
"errors"
"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"
)
// HTTPDNSDomainService HTTPDNS域名服务
type HTTPDNSDomainService struct {
services.BaseService
pb.UnimplementedHTTPDNSDomainServiceServer
}
func (this *HTTPDNSDomainService) CreateHTTPDNSDomain(ctx context.Context, req *pb.CreateHTTPDNSDomainRequest) (*pb.CreateHTTPDNSDomainResponse, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
if req.AppDbId <= 0 || len(req.Domain) == 0 {
return nil, errors.New("required 'appDbId' and 'domain'")
}
var domainId int64
err = this.RunTx(func(tx *dbs.Tx) error {
app, err := ensureAppAccess(tx, req.AppDbId, userId)
if err != nil {
return err
}
if app == nil {
return errors.New("app not found")
}
domainId, err = models.SharedHTTPDNSDomainDAO.CreateDomain(tx, req.AppDbId, req.Domain, req.IsOn)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, req.AppDbId, models.HTTPDNSNodeTaskTypeDomainChanged)
})
if err != nil {
return nil, err
}
return &pb.CreateHTTPDNSDomainResponse{DomainId: domainId}, nil
}
func (this *HTTPDNSDomainService) DeleteHTTPDNSDomain(ctx context.Context, req *pb.DeleteHTTPDNSDomainRequest) (*pb.RPCSuccess, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
domain, app, err := ensureDomainAccess(tx, req.DomainId, userId)
if err != nil {
return err
}
if domain == nil {
return nil
}
err = models.SharedHTTPDNSDomainDAO.DisableDomain(tx, req.DomainId)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, int64(app.Id), models.HTTPDNSNodeTaskTypeDomainChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSDomainService) UpdateHTTPDNSDomainStatus(ctx context.Context, req *pb.UpdateHTTPDNSDomainStatusRequest) (*pb.RPCSuccess, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
domain, app, err := ensureDomainAccess(tx, req.DomainId, userId)
if err != nil {
return err
}
if domain == nil {
return nil
}
err = models.SharedHTTPDNSDomainDAO.UpdateDomainStatus(tx, req.DomainId, req.IsOn)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, int64(app.Id), models.HTTPDNSNodeTaskTypeDomainChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSDomainService) ListHTTPDNSDomainsWithAppId(ctx context.Context, req *pb.ListHTTPDNSDomainsWithAppIdRequest) (*pb.ListHTTPDNSDomainsWithAppIdResponse, error) {
_, userId, validateErr := this.ValidateAdminAndUser(ctx, true)
if validateErr != nil {
if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
return nil, validateErr
}
} else if userId > 0 {
app, err := ensureAppAccess(this.NullTx(), req.AppDbId, userId)
if err != nil {
return nil, err
}
if app == nil {
return &pb.ListHTTPDNSDomainsWithAppIdResponse{}, nil
}
}
domains, err := models.SharedHTTPDNSDomainDAO.ListEnabledDomainsWithAppId(this.NullTx(), req.AppDbId, req.Keyword)
if err != nil {
return nil, err
}
var pbDomains []*pb.HTTPDNSDomain
for _, domain := range domains {
ruleCount, err := models.SharedHTTPDNSCustomRuleDAO.CountEnabledRulesWithDomainId(this.NullTx(), int64(domain.Id))
if err != nil {
return nil, err
}
pbDomains = append(pbDomains, toPBDomain(domain, ruleCount))
}
return &pb.ListHTTPDNSDomainsWithAppIdResponse{Domains: pbDomains}, nil
}

View File

@@ -0,0 +1,409 @@
package httpdns
import (
"context"
"encoding/json"
"errors"
"io"
"path/filepath"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"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"
"github.com/iwind/TeaGo/dbs"
"github.com/iwind/TeaGo/logs"
stringutil "github.com/iwind/TeaGo/utils/string"
)
// HTTPDNSNodeService HTTPDNS节点服务
type HTTPDNSNodeService struct {
services.BaseService
pb.UnimplementedHTTPDNSNodeServiceServer
}
func (this *HTTPDNSNodeService) CreateHTTPDNSNode(ctx context.Context, req *pb.CreateHTTPDNSNodeRequest) (*pb.CreateHTTPDNSNodeResponse, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
if req.ClusterId <= 0 {
return nil, errors.New("required 'clusterId'")
}
var nodeId int64
err = this.RunTx(func(tx *dbs.Tx) error {
nodeId, err = models.SharedHTTPDNSNodeDAO.CreateNode(tx, req.ClusterId, req.Name, req.InstallDir, req.IsOn)
if err != nil {
return err
}
return notifyHTTPDNSClusterTask(tx, req.ClusterId, models.HTTPDNSNodeTaskTypeConfigChanged)
})
if err != nil {
return nil, err
}
return &pb.CreateHTTPDNSNodeResponse{NodeId: nodeId}, nil
}
func (this *HTTPDNSNodeService) UpdateHTTPDNSNode(ctx context.Context, req *pb.UpdateHTTPDNSNodeRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
node, err := models.SharedHTTPDNSNodeDAO.FindEnabledNode(tx, req.NodeId)
if err != nil {
return err
}
if node == nil {
return errors.New("node not found")
}
err = models.SharedHTTPDNSNodeDAO.UpdateNode(tx, req.NodeId, req.Name, req.InstallDir, req.IsOn)
if err != nil {
return err
}
return notifyHTTPDNSClusterTask(tx, int64(node.ClusterId), models.HTTPDNSNodeTaskTypeConfigChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSNodeService) DeleteHTTPDNSNode(ctx context.Context, req *pb.DeleteHTTPDNSNodeRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
node, err := models.SharedHTTPDNSNodeDAO.FindEnabledNode(tx, req.NodeId)
if err != nil {
return err
}
if node == nil {
return nil
}
err = models.SharedHTTPDNSNodeDAO.DisableNode(tx, req.NodeId)
if err != nil {
return err
}
return notifyHTTPDNSClusterTask(tx, int64(node.ClusterId), models.HTTPDNSNodeTaskTypeConfigChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSNodeService) FindHTTPDNSNode(ctx context.Context, req *pb.FindHTTPDNSNodeRequest) (*pb.FindHTTPDNSNodeResponse, error) {
nodeId := req.NodeId
if nodeId <= 0 {
parsedNodeId, nodeErr := this.ValidateHTTPDNSNode(ctx)
if nodeErr != nil {
return nil, errors.New("invalid 'nodeId'")
}
nodeId = parsedNodeId
} else {
_, _, validateErr := this.ValidateAdminAndUser(ctx, true)
if validateErr != nil {
if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
return nil, validateErr
}
}
}
node, err := models.SharedHTTPDNSNodeDAO.FindEnabledNode(this.NullTx(), nodeId)
if err != nil {
return nil, err
}
pbNode := toPBNode(node)
// 认证信息
if pbNode != nil {
login, loginErr := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(this.NullTx(), nodeconfigs.NodeRoleHTTPDNS, nodeId)
if loginErr != nil {
return nil, loginErr
}
if login != nil {
pbNode.NodeLogin = &pb.NodeLogin{
Id: int64(login.Id),
Name: login.Name,
Type: login.Type,
Params: login.Params,
}
}
}
return &pb.FindHTTPDNSNodeResponse{Node: pbNode}, nil
}
func (this *HTTPDNSNodeService) ListHTTPDNSNodes(ctx context.Context, req *pb.ListHTTPDNSNodesRequest) (*pb.ListHTTPDNSNodesResponse, error) {
_, _, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
nodes, err := models.SharedHTTPDNSNodeDAO.ListEnabledNodes(this.NullTx(), req.ClusterId)
if err != nil {
return nil, err
}
var pbNodes []*pb.HTTPDNSNode
for _, node := range nodes {
pbNodes = append(pbNodes, toPBNode(node))
}
return &pb.ListHTTPDNSNodesResponse{Nodes: pbNodes}, nil
}
func (this *HTTPDNSNodeService) UpdateHTTPDNSNodeStatus(ctx context.Context, req *pb.UpdateHTTPDNSNodeStatusRequest) (*pb.RPCSuccess, error) {
nodeId := req.GetNodeId()
isAdminCaller := false
if nodeId > 0 {
if _, adminErr := this.ValidateAdmin(ctx); adminErr == nil {
isAdminCaller = true
}
}
if !isAdminCaller {
if nodeId <= 0 {
parsedNodeId, err := this.ValidateHTTPDNSNode(ctx)
if err != nil {
return nil, err
}
nodeId = parsedNodeId
}
}
if nodeId <= 0 {
return nil, errors.New("invalid 'nodeId'")
}
err := models.SharedHTTPDNSNodeDAO.UpdateNodeStatus(this.NullTx(), nodeId, req.GetIsUp(), req.GetIsInstalled(), req.GetIsActive(), req.GetStatusJSON(), req.GetInstallStatusJSON())
if err != nil {
return nil, err
}
if isAdminCaller && shouldTriggerHTTPDNSInstall(req.GetInstallStatusJSON()) {
goman.New(func() {
installErr := installers.SharedHTTPDNSNodeQueue().InstallNodeProcess(nodeId, false)
if installErr != nil {
logs.Println("[RPC][HTTPDNS]install node failed:", installErr.Error())
}
})
}
return this.Success()
}
// UpdateHTTPDNSNodeLogin 修改HTTPDNS节点登录信息
func (this *HTTPDNSNodeService) UpdateHTTPDNSNodeLogin(ctx context.Context, req *pb.UpdateHTTPDNSNodeLoginRequest) (*pb.RPCSuccess, error) {
_, err := this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
var tx = this.NullTx()
if req.NodeLogin.Id <= 0 {
loginId, createErr := models.SharedNodeLoginDAO.CreateNodeLogin(tx, nodeconfigs.NodeRoleHTTPDNS, req.NodeId, req.NodeLogin.Name, req.NodeLogin.Type, req.NodeLogin.Params)
if createErr != nil {
return nil, createErr
}
req.NodeLogin.Id = loginId
}
err = models.SharedNodeLoginDAO.UpdateNodeLogin(tx, req.NodeLogin.Id, req.NodeLogin.Name, req.NodeLogin.Type, req.NodeLogin.Params)
if err != nil {
return nil, err
}
return this.Success()
}
// CheckHTTPDNSNodeLatestVersion 检查HTTPDNS节点新版本
func (this *HTTPDNSNodeService) CheckHTTPDNSNodeLatestVersion(ctx context.Context, req *pb.CheckHTTPDNSNodeLatestVersionRequest) (*pb.CheckHTTPDNSNodeLatestVersionResponse, error) {
_, _, _, err := rpcutils.ValidateRequest(ctx, rpcutils.UserTypeAdmin, rpcutils.UserTypeHTTPDNS)
if err != nil {
return nil, err
}
deployFiles := installers.SharedDeployManager.LoadHTTPDNSNodeFiles()
for _, file := range deployFiles {
if file.OS == req.Os && file.Arch == req.Arch && stringutil.VersionCompare(file.Version, req.CurrentVersion) > 0 {
return &pb.CheckHTTPDNSNodeLatestVersionResponse{
HasNewVersion: true,
NewVersion: file.Version,
}, nil
}
}
return &pb.CheckHTTPDNSNodeLatestVersionResponse{HasNewVersion: false}, nil
}
// DownloadHTTPDNSNodeInstallationFile 下载最新HTTPDNS节点安装文件
func (this *HTTPDNSNodeService) DownloadHTTPDNSNodeInstallationFile(ctx context.Context, req *pb.DownloadHTTPDNSNodeInstallationFileRequest) (*pb.DownloadHTTPDNSNodeInstallationFileResponse, error) {
nodeId, err := this.ValidateHTTPDNSNode(ctx)
if err != nil {
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
}
sum, err := file.Sum()
if err != nil {
return nil, err
}
data, offset, err := file.Read(req.ChunkOffset)
if err != nil && err != io.EOF {
return nil, err
}
// 增加下载速度监控
installers.SharedUpgradeLimiter.UpdateNodeBytes(nodeconfigs.NodeRoleHTTPDNS, nodeId, int64(len(data)))
return &pb.DownloadHTTPDNSNodeInstallationFileResponse{
Sum: sum,
Offset: offset,
ChunkData: data,
Version: file.Version,
Filename: filepath.Base(file.Path),
}, 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
}
installStatus := &models.NodeInstallStatus{}
err := json.Unmarshal(installStatusJSON, installStatus)
if err != nil {
return false
}
return installStatus.IsRunning && !installStatus.IsFinished
}

View File

@@ -0,0 +1,206 @@
package httpdns
import (
"context"
"errors"
"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"
)
// HTTPDNSRuleService HTTPDNS规则服务
type HTTPDNSRuleService struct {
services.BaseService
pb.UnimplementedHTTPDNSRuleServiceServer
}
func (this *HTTPDNSRuleService) CreateHTTPDNSCustomRule(ctx context.Context, req *pb.CreateHTTPDNSCustomRuleRequest) (*pb.CreateHTTPDNSCustomRuleResponse, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
if req.Rule == nil {
return nil, errors.New("required 'rule'")
}
var ruleId int64
err = this.RunTx(func(tx *dbs.Tx) error {
domain, app, err := ensureDomainAccess(tx, req.Rule.DomainId, userId)
if err != nil {
return err
}
if domain == nil || app == nil {
return errors.New("domain not found")
}
rule := &models.HTTPDNSCustomRule{
AppId: domain.AppId,
DomainId: uint32(req.Rule.DomainId),
RuleName: req.Rule.RuleName,
LineScope: req.Rule.LineScope,
LineCarrier: req.Rule.LineCarrier,
LineRegion: req.Rule.LineRegion,
LineProvince: req.Rule.LineProvince,
LineContinent: req.Rule.LineContinent,
LineCountry: req.Rule.LineCountry,
TTL: req.Rule.Ttl,
IsOn: req.Rule.IsOn,
Priority: req.Rule.Priority,
}
ruleId, err = models.SharedHTTPDNSCustomRuleDAO.CreateRule(tx, rule)
if err != nil {
return err
}
for _, record := range req.Rule.Records {
_, err := models.SharedHTTPDNSCustomRuleRecordDAO.CreateRecord(tx, ruleId, record.RecordType, record.RecordValue, record.Weight, record.Sort)
if err != nil {
return err
}
}
return notifyHTTPDNSAppTasksByAppDbId(tx, int64(app.Id), models.HTTPDNSNodeTaskTypeRuleChanged)
})
if err != nil {
return nil, err
}
return &pb.CreateHTTPDNSCustomRuleResponse{RuleId: ruleId}, nil
}
func (this *HTTPDNSRuleService) UpdateHTTPDNSCustomRule(ctx context.Context, req *pb.UpdateHTTPDNSCustomRuleRequest) (*pb.RPCSuccess, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
if req.Rule == nil || req.Rule.Id <= 0 {
return nil, errors.New("invalid 'rule.id'")
}
err = this.RunTx(func(tx *dbs.Tx) error {
oldRule, app, err := ensureRuleAccess(tx, req.Rule.Id, userId)
if err != nil {
return err
}
if oldRule == nil {
return errors.New("rule not found")
}
rule := &models.HTTPDNSCustomRule{
Id: uint32(req.Rule.Id),
RuleName: req.Rule.RuleName,
LineScope: req.Rule.LineScope,
LineCarrier: req.Rule.LineCarrier,
LineRegion: req.Rule.LineRegion,
LineProvince: req.Rule.LineProvince,
LineContinent: req.Rule.LineContinent,
LineCountry: req.Rule.LineCountry,
TTL: req.Rule.Ttl,
IsOn: req.Rule.IsOn,
Priority: req.Rule.Priority,
}
err = models.SharedHTTPDNSCustomRuleDAO.UpdateRule(tx, rule)
if err != nil {
return err
}
err = models.SharedHTTPDNSCustomRuleRecordDAO.DisableRecordsWithRuleId(tx, req.Rule.Id)
if err != nil {
return err
}
for _, record := range req.Rule.Records {
_, err := models.SharedHTTPDNSCustomRuleRecordDAO.CreateRecord(tx, req.Rule.Id, record.RecordType, record.RecordValue, record.Weight, record.Sort)
if err != nil {
return err
}
}
err = notifyHTTPDNSAppTasksByAppDbId(tx, int64(app.Id), models.HTTPDNSNodeTaskTypeRuleChanged)
if err != nil {
return err
}
targetAppDbId := int64(app.Id)
return notifyHTTPDNSAppTasksByAppDbId(tx, targetAppDbId, models.HTTPDNSNodeTaskTypeRuleChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSRuleService) DeleteHTTPDNSCustomRule(ctx context.Context, req *pb.DeleteHTTPDNSCustomRuleRequest) (*pb.RPCSuccess, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
rule, app, err := ensureRuleAccess(tx, req.RuleId, userId)
if err != nil {
return err
}
if rule == nil {
return nil
}
err = models.SharedHTTPDNSCustomRuleDAO.DisableRule(tx, req.RuleId)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, int64(app.Id), models.HTTPDNSNodeTaskTypeRuleChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSRuleService) UpdateHTTPDNSCustomRuleStatus(ctx context.Context, req *pb.UpdateHTTPDNSCustomRuleStatusRequest) (*pb.RPCSuccess, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
err = this.RunTx(func(tx *dbs.Tx) error {
rule, app, err := ensureRuleAccess(tx, req.RuleId, userId)
if err != nil {
return err
}
if rule == nil {
return nil
}
err = models.SharedHTTPDNSCustomRuleDAO.UpdateRuleStatus(tx, req.RuleId, req.IsOn)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByAppDbId(tx, int64(app.Id), models.HTTPDNSNodeTaskTypeRuleChanged)
})
if err != nil {
return nil, err
}
return this.Success()
}
func (this *HTTPDNSRuleService) ListHTTPDNSCustomRulesWithDomainId(ctx context.Context, req *pb.ListHTTPDNSCustomRulesWithDomainIdRequest) (*pb.ListHTTPDNSCustomRulesWithDomainIdResponse, error) {
_, userId, validateErr := this.ValidateAdminAndUser(ctx, true)
if validateErr != nil {
if _, nodeErr := this.ValidateHTTPDNSNode(ctx); nodeErr != nil {
return nil, validateErr
}
} else if userId > 0 {
domain, _, err := ensureDomainAccess(this.NullTx(), req.DomainId, userId)
if err != nil {
return nil, err
}
if domain == nil {
return &pb.ListHTTPDNSCustomRulesWithDomainIdResponse{}, nil
}
}
rules, err := models.SharedHTTPDNSCustomRuleDAO.ListEnabledRulesWithDomainId(this.NullTx(), req.DomainId)
if err != nil {
return nil, err
}
var pbRules []*pb.HTTPDNSCustomRule
for _, rule := range rules {
records, err := models.SharedHTTPDNSCustomRuleRecordDAO.ListEnabledRecordsWithRuleId(this.NullTx(), int64(rule.Id))
if err != nil {
return nil, err
}
pbRules = append(pbRules, toPBRule(rule, records))
}
return &pb.ListHTTPDNSCustomRulesWithDomainIdResponse{Rules: pbRules}, nil
}

View File

@@ -0,0 +1,107 @@
package httpdns
import (
"context"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/services"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
timeutil "github.com/iwind/TeaGo/utils/time"
"time"
)
// HTTPDNSRuntimeLogService HTTPDNS运行日志服务
type HTTPDNSRuntimeLogService struct {
services.BaseService
pb.UnimplementedHTTPDNSRuntimeLogServiceServer
}
func (this *HTTPDNSRuntimeLogService) CreateHTTPDNSRuntimeLogs(ctx context.Context, req *pb.CreateHTTPDNSRuntimeLogsRequest) (*pb.CreateHTTPDNSRuntimeLogsResponse, error) {
nodeIdInContext, err := this.ValidateHTTPDNSNode(ctx)
if err != nil {
_, err = this.ValidateAdmin(ctx)
if err != nil {
return nil, err
}
}
for _, item := range req.Logs {
createdAt := item.CreatedAt
if createdAt <= 0 {
createdAt = time.Now().Unix()
}
day := item.Day
if len(day) == 0 {
day = timeutil.Format("Ymd")
}
nodeId := item.NodeId
// When called by HTTPDNS node, trust node id parsed from RPC token.
if nodeIdInContext > 0 {
nodeId = nodeIdInContext
}
clusterId := item.ClusterId
if clusterId <= 0 && nodeId > 0 {
clusterId, _ = models.SharedHTTPDNSNodeDAO.FindNodeClusterId(this.NullTx(), nodeId)
}
log := &models.HTTPDNSRuntimeLog{
ClusterId: uint32(clusterId),
NodeId: uint32(nodeId),
Level: item.Level,
Type: item.Type,
Module: item.Module,
Description: item.Description,
Count: item.Count,
RequestId: item.RequestId,
CreatedAt: uint64(createdAt),
Day: day,
}
err := models.SharedHTTPDNSRuntimeLogDAO.CreateLog(this.NullTx(), log)
if err != nil {
return nil, err
}
}
return &pb.CreateHTTPDNSRuntimeLogsResponse{}, nil
}
func (this *HTTPDNSRuntimeLogService) ListHTTPDNSRuntimeLogs(ctx context.Context, req *pb.ListHTTPDNSRuntimeLogsRequest) (*pb.ListHTTPDNSRuntimeLogsResponse, error) {
_, _, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
total, err := models.SharedHTTPDNSRuntimeLogDAO.CountLogs(this.NullTx(), req.Day, req.ClusterId, req.NodeId, req.Level, req.Keyword)
if err != nil {
return nil, err
}
logs, err := models.SharedHTTPDNSRuntimeLogDAO.ListLogs(this.NullTx(), req.Day, req.ClusterId, req.NodeId, req.Level, req.Keyword, req.Offset, req.Size)
if err != nil {
return nil, err
}
var pbLogs []*pb.HTTPDNSRuntimeLog
for _, item := range logs {
clusterName, _ := models.SharedHTTPDNSClusterDAO.FindEnabledClusterName(this.NullTx(), int64(item.ClusterId))
nodeName := ""
node, _ := models.SharedHTTPDNSNodeDAO.FindEnabledNode(this.NullTx(), int64(item.NodeId))
if node != nil {
nodeName = node.Name
}
pbLogs = append(pbLogs, &pb.HTTPDNSRuntimeLog{
Id: int64(item.Id),
ClusterId: int64(item.ClusterId),
NodeId: int64(item.NodeId),
Level: item.Level,
Type: item.Type,
Module: item.Module,
Description: item.Description,
Count: item.Count,
RequestId: item.RequestId,
CreatedAt: int64(item.CreatedAt),
Day: item.Day,
ClusterName: clusterName,
NodeName: nodeName,
})
}
return &pb.ListHTTPDNSRuntimeLogsResponse{
Logs: pbLogs,
Total: total,
}, nil
}

View File

@@ -0,0 +1,285 @@
package httpdns
import (
"context"
"crypto/hmac"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"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/rands"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
// HTTPDNSSandboxService HTTPDNS解析测试服务
type HTTPDNSSandboxService struct {
services.BaseService
pb.UnimplementedHTTPDNSSandboxServiceServer
}
// nodeResolveResponse 节点返回的 JSON 结构(对齐 EdgeHttpDNS resolve_server.go
type nodeResolveResponse struct {
Code string `json:"code"`
Message string `json:"message"`
RequestID string `json:"requestId"`
Data *nodeResolveData `json:"data,omitempty"`
}
type nodeResolveData struct {
Domain string `json:"domain"`
QType string `json:"qtype"`
TTL int32 `json:"ttl"`
Records []*nodeResolveRecord `json:"records"`
Client *nodeClientInfo `json:"client"`
Summary string `json:"summary"`
}
type nodeResolveRecord struct {
Type string `json:"type"`
IP string `json:"ip"`
Weight int32 `json:"weight"`
Line string `json:"line"`
Region string `json:"region"`
}
type nodeClientInfo struct {
IP string `json:"ip"`
Region string `json:"region"`
Carrier string `json:"carrier"`
Country string `json:"country"`
}
func (this *HTTPDNSSandboxService) TestHTTPDNSResolve(ctx context.Context, req *pb.TestHTTPDNSResolveRequest) (*pb.TestHTTPDNSResolveResponse, error) {
_, userId, err := this.ValidateAdminAndUser(ctx, true)
if err != nil {
return nil, err
}
if len(req.AppId) == 0 || len(req.Domain) == 0 {
return nil, errors.New("appId 和 domain 不能为空")
}
app, err := models.SharedHTTPDNSAppDAO.FindEnabledAppWithAppId(this.NullTx(), req.AppId)
if err != nil {
return nil, err
}
if userId > 0 && app != nil && app.UserId != userId {
return nil, errors.New("access denied")
}
if app == nil || !app.IsOn {
return &pb.TestHTTPDNSResolveResponse{
Code: "APP_NOT_FOUND_OR_DISABLED",
Message: "找不到指定的应用,或该应用已下线",
RequestId: "rid-" + rands.HexString(12),
}, nil
}
// 检查集群是否绑定
appClusterIds := models.SharedHTTPDNSAppDAO.ReadAppClusterIds(app)
if req.ClusterId > 0 {
var found bool
for _, cid := range appClusterIds {
if cid == req.ClusterId {
found = true
break
}
}
if !found {
return &pb.TestHTTPDNSResolveResponse{
Code: "APP_CLUSTER_MISMATCH",
Message: "当前应用未绑定到该集群",
RequestId: "rid-" + rands.HexString(12),
}, nil
}
}
qtype := strings.ToUpper(strings.TrimSpace(req.Qtype))
if qtype == "" {
qtype = "A"
}
// 获取集群服务域名
clusterId := req.ClusterId
if clusterId <= 0 && len(appClusterIds) > 0 {
clusterId = appClusterIds[0]
}
cluster, err := models.SharedHTTPDNSClusterDAO.FindEnabledCluster(this.NullTx(), clusterId)
if err != nil {
return nil, err
}
if cluster == nil {
return &pb.TestHTTPDNSResolveResponse{
Code: "CLUSTER_NOT_FOUND",
Message: "找不到指定的集群",
RequestId: "rid-" + rands.HexString(12),
}, nil
}
serviceDomain := strings.TrimSpace(cluster.ServiceDomain)
if len(serviceDomain) == 0 {
return &pb.TestHTTPDNSResolveResponse{
Code: "NO_SERVICE_DOMAIN",
Message: "该集群未配置服务域名",
RequestId: "rid-" + rands.HexString(12),
}, nil
}
// 构造请求转发到 EdgeHttpDNS 节点
secret, err := models.SharedHTTPDNSAppSecretDAO.FindEnabledAppSecret(this.NullTx(), int64(app.Id))
if err != nil {
return nil, err
}
port := "443"
if len(cluster.TLSPolicy) > 0 {
var tlsConfig map[string]interface{}
if err := json.Unmarshal(cluster.TLSPolicy, &tlsConfig); err == nil {
if listenRaw, ok := tlsConfig["listen"]; ok && listenRaw != nil {
if data, err := json.Marshal(listenRaw); err == nil {
var listenAddresses []map[string]interface{}
if err := json.Unmarshal(data, &listenAddresses); err == nil {
if len(listenAddresses) > 0 {
if portRange, ok := listenAddresses[0]["portRange"].(string); ok && len(portRange) > 0 {
port = portRange
}
}
}
}
}
}
}
query := url.Values{}
query.Set("appId", req.AppId)
query.Set("dn", req.Domain)
query.Set("qtype", qtype)
if len(req.ClientIP) > 0 {
query.Set("cip", req.ClientIP)
}
if len(req.Sid) > 0 {
query.Set("sid", req.Sid)
}
if len(req.SdkVersion) > 0 {
query.Set("sdk_version", req.SdkVersion)
}
if len(req.Os) > 0 {
query.Set("os", req.Os)
}
// 应用开启验签时,沙盒自动生成签名参数,避免测试请求被拒绝
if secret != nil && secret.SignEnabled {
signSecret := strings.TrimSpace(secret.SignSecret)
if len(signSecret) == 0 {
return &pb.TestHTTPDNSResolveResponse{
Code: "SIGN_INVALID",
Message: "应用开启了请求验签,但未配置有效加签 Secret",
RequestId: "rid-" + rands.HexString(12),
Domain: req.Domain,
Qtype: qtype,
}, nil
}
exp := strconv.FormatInt(time.Now().Unix()+300, 10)
nonce := "sandbox-" + rands.HexString(16)
sign := buildSandboxResolveSign(signSecret, req.AppId, req.Domain, qtype, exp, nonce)
query.Set("exp", exp)
query.Set("nonce", nonce)
query.Set("sign", sign)
}
resolveURL := "https://" + serviceDomain + ":" + port + "/resolve?" + query.Encode()
httpClient := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // 沙盒测试环境允许自签名证书
},
},
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, resolveURL, nil)
if err != nil {
return nil, fmt.Errorf("构建请求失败: %w", err)
}
resp, err := httpClient.Do(httpReq)
if err != nil {
return &pb.TestHTTPDNSResolveResponse{
Code: "NODE_UNREACHABLE",
Message: "无法连接到 HTTPDNS 节点: " + err.Error(),
RequestId: "rid-" + rands.HexString(12),
Domain: req.Domain,
Qtype: qtype,
}, nil
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
if err != nil {
return nil, fmt.Errorf("读取节点响应失败: %w", err)
}
// 解析节点返回的 JSON
var nodeResp nodeResolveResponse
if err := json.Unmarshal(body, &nodeResp); err != nil {
return &pb.TestHTTPDNSResolveResponse{
Code: "PARSE_ERROR",
Message: "解析节点返回数据失败: " + err.Error(),
RequestId: "rid-" + rands.HexString(12),
Domain: req.Domain,
Qtype: qtype,
}, nil
}
// 映射节点响应到 protobuf 响应
pbResp := &pb.TestHTTPDNSResolveResponse{
Code: nodeResp.Code,
Message: nodeResp.Message,
RequestId: nodeResp.RequestID,
Domain: req.Domain,
Qtype: qtype,
}
if nodeResp.Data != nil {
pbResp.Ttl = nodeResp.Data.TTL
pbResp.Summary = nodeResp.Data.Summary
if nodeResp.Data.Client != nil {
pbResp.ClientIP = nodeResp.Data.Client.IP
pbResp.ClientRegion = nodeResp.Data.Client.Region
pbResp.ClientCarrier = nodeResp.Data.Client.Carrier
pbResp.ClientCountry = nodeResp.Data.Client.Country
}
for _, rec := range nodeResp.Data.Records {
pbResp.Records = append(pbResp.Records, &pb.HTTPDNSResolveRecord{
Type: rec.Type,
Ip: rec.IP,
Ttl: nodeResp.Data.TTL,
Weight: rec.Weight,
Line: rec.Line,
Region: rec.Region,
})
}
}
return pbResp, nil
}
func buildSandboxResolveSign(signSecret string, appID string, domain string, qtype string, exp string, nonce string) string {
raw := strings.TrimSpace(appID) + "|" + strings.ToLower(strings.TrimSpace(domain)) + "|" + strings.ToUpper(strings.TrimSpace(qtype)) + "|" + strings.TrimSpace(exp) + "|" + strings.TrimSpace(nonce)
mac := hmac.New(sha256.New, []byte(strings.TrimSpace(signSecret)))
_, _ = mac.Write([]byte(raw))
return hex.EncodeToString(mac.Sum(nil))
}

View File

@@ -0,0 +1,47 @@
package httpdns
import (
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/iwind/TeaGo/dbs"
)
func notifyHTTPDNSClusterTask(tx *dbs.Tx, clusterId int64, taskType models.NodeTaskType) error {
if clusterId <= 0 {
return nil
}
return models.SharedNodeTaskDAO.CreateClusterTask(tx, nodeconfigs.NodeRoleHTTPDNS, clusterId, 0, 0, taskType)
}
func notifyHTTPDNSAppTasksByApp(tx *dbs.Tx, app *models.HTTPDNSApp, taskType models.NodeTaskType) error {
if app == nil {
return nil
}
clusterIds := models.SharedHTTPDNSAppDAO.ReadAppClusterIds(app)
notified := map[int64]bool{}
for _, clusterId := range clusterIds {
if clusterId <= 0 || notified[clusterId] {
continue
}
notified[clusterId] = true
err := notifyHTTPDNSClusterTask(tx, clusterId, taskType)
if err != nil {
return err
}
}
return nil
}
func notifyHTTPDNSAppTasksByAppDbId(tx *dbs.Tx, appDbId int64, taskType models.NodeTaskType) error {
if appDbId <= 0 {
return nil
}
app, err := models.SharedHTTPDNSAppDAO.FindEnabledApp(tx, appDbId)
if err != nil {
return err
}
return notifyHTTPDNSAppTasksByApp(tx, app, taskType)
}

View File

@@ -0,0 +1,81 @@
package httpdns
import (
"errors"
"strings"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/iwind/TeaGo/dbs"
)
func ensureAppAccess(tx *dbs.Tx, appDbId int64, userId int64) (*models.HTTPDNSApp, error) {
app, err := models.SharedHTTPDNSAppDAO.FindEnabledApp(tx, appDbId)
if err != nil {
return nil, err
}
if app == nil {
return nil, nil
}
if userId > 0 && app.UserId != userId {
return nil, errors.New("access denied")
}
return app, nil
}
func ensureAppAccessByAppId(tx *dbs.Tx, appId string, userId int64) (*models.HTTPDNSApp, error) {
appId = strings.TrimSpace(appId)
if len(appId) == 0 {
return nil, nil
}
app, err := models.SharedHTTPDNSAppDAO.FindEnabledAppWithAppId(tx, appId)
if err != nil {
return nil, err
}
if app == nil {
return nil, nil
}
if userId > 0 && app.UserId != userId {
return nil, errors.New("access denied")
}
return app, nil
}
func ensureDomainAccess(tx *dbs.Tx, domainId int64, userId int64) (*models.HTTPDNSDomain, *models.HTTPDNSApp, error) {
domain, err := models.SharedHTTPDNSDomainDAO.FindEnabledDomain(tx, domainId)
if err != nil {
return nil, nil, err
}
if domain == nil {
return nil, nil, nil
}
app, err := ensureAppAccess(tx, int64(domain.AppId), userId)
if err != nil {
return nil, nil, err
}
if app == nil {
return nil, nil, nil
}
return domain, app, nil
}
func ensureRuleAccess(tx *dbs.Tx, ruleId int64, userId int64) (*models.HTTPDNSCustomRule, *models.HTTPDNSApp, error) {
rule, err := models.SharedHTTPDNSCustomRuleDAO.FindEnabledRule(tx, ruleId)
if err != nil {
return nil, nil, err
}
if rule == nil {
return nil, nil, nil
}
app, err := ensureAppAccess(tx, int64(rule.AppId), userId)
if err != nil {
return nil, nil, err
}
if app == nil {
return nil, nil, nil
}
return rule, app, nil
}

View File

@@ -6,12 +6,12 @@ package nameservers
import (
"context"
"encoding/json"
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"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"
@@ -474,7 +474,7 @@ func (this *NSNodeService) FindLatestNSNodeVersion(ctx context.Context, req *pb.
return nil, err
}
return &pb.FindLatestNSNodeVersionResponse{Version: teaconst.DNSNodeVersion}, nil
return &pb.FindLatestNSNodeVersionResponse{Version: installers.SharedDeployManager.LatestNSNodeVersion()}, nil
}
// DownloadNSNodeInstallationFile 下载最新DNS节点安装文件
@@ -484,6 +484,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 +744,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()
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/db/models/stats"
"github.com/TeaOSLab/EdgeAPI/internal/errors"
"github.com/TeaOSLab/EdgeAPI/internal/installers"
"github.com/TeaOSLab/EdgeAPI/internal/rpc/tasks"
rpcutils "github.com/TeaOSLab/EdgeAPI/internal/rpc/utils"
"github.com/TeaOSLab/EdgeAPI/internal/utils"
@@ -689,17 +690,20 @@ func (this *AdminService) ComposeAdminDashboard(ctx context.Context, req *pb.Com
// 边缘节点升级信息
{
upgradeInfo := &pb.ComposeAdminDashboardResponse_UpgradeInfo{
NewVersion: teaconst.NodeVersion,
var nodeVersion = installers.SharedDeployManager.LatestNodeVersion()
if len(nodeVersion) > 0 {
upgradeInfo := &pb.ComposeAdminDashboardResponse_UpgradeInfo{
NewVersion: nodeVersion,
}
this.BeginTag(ctx, "SharedNodeDAO.CountAllLowerVersionNodes")
countNodes, err := models.SharedNodeDAO.CountAllLowerVersionNodes(tx, upgradeInfo.NewVersion)
this.EndTag(ctx, "SharedNodeDAO.CountAllLowerVersionNodes")
if err != nil {
return nil, err
}
upgradeInfo.CountNodes = countNodes
result.NodeUpgradeInfo = upgradeInfo
}
this.BeginTag(ctx, "SharedNodeDAO.CountAllLowerVersionNodes")
countNodes, err := models.SharedNodeDAO.CountAllLowerVersionNodes(tx, upgradeInfo.NewVersion)
this.EndTag(ctx, "SharedNodeDAO.CountAllLowerVersionNodes")
if err != nil {
return nil, err
}
upgradeInfo.CountNodes = countNodes
result.NodeUpgradeInfo = upgradeInfo
}
// API节点升级信息

View File

@@ -7,6 +7,7 @@ import (
"context"
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeAPI/internal/installers"
"github.com/TeaOSLab/EdgeAPI/internal/db/models/regions"
"github.com/TeaOSLab/EdgeAPI/internal/db/models/stats"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
@@ -36,17 +37,20 @@ func (this *AdminService) composeAdminDashboardExt(tx *dbs.Tx, ctx context.Conte
// DNS节点升级信息
if isPlus {
upgradeInfo := &pb.ComposeAdminDashboardResponse_UpgradeInfo{
NewVersion: teaconst.DNSNodeVersion,
var dnsNodeVersion = installers.SharedDeployManager.LatestNSNodeVersion()
if len(dnsNodeVersion) > 0 {
upgradeInfo := &pb.ComposeAdminDashboardResponse_UpgradeInfo{
NewVersion: dnsNodeVersion,
}
this.BeginTag(ctx, "SharedNSNodeDAO.CountAllLowerVersionNodes")
countNodes, err := models.SharedNSNodeDAO.CountAllLowerVersionNodes(tx, upgradeInfo.NewVersion)
this.EndTag(ctx, "SharedNSNodeDAO.CountAllLowerVersionNodes")
if err != nil {
return err
}
upgradeInfo.CountNodes = countNodes
result.NsNodeUpgradeInfo = upgradeInfo
}
this.BeginTag(ctx, "SharedNSNodeDAO.CountAllLowerVersionNodes")
countNodes, err := models.SharedNSNodeDAO.CountAllLowerVersionNodes(tx, upgradeInfo.NewVersion)
this.EndTag(ctx, "SharedNSNodeDAO.CountAllLowerVersionNodes")
if err != nil {
return err
}
upgradeInfo.CountNodes = countNodes
result.NsNodeUpgradeInfo = upgradeInfo
}
// Report节点升级信息

View File

@@ -83,6 +83,12 @@ func (this *BaseService) ValidateNSNode(ctx context.Context) (nodeId int64, err
return
}
// ValidateHTTPDNSNode 校验HTTPDNS节点
func (this *BaseService) ValidateHTTPDNSNode(ctx context.Context) (nodeId int64, err error) {
_, _, nodeId, err = rpcutils.ValidateRequest(ctx, rpcutils.UserTypeHTTPDNS)
return
}
// ValidateUserNode 校验用户节点
func (this *BaseService) ValidateUserNode(ctx context.Context, canRest bool) (userId int64, err error) {
// 不允许REST调用
@@ -105,7 +111,7 @@ func (this *BaseService) ValidateAuthorityNode(ctx context.Context) (nodeId int6
func (this *BaseService) ValidateNodeId(ctx context.Context, roles ...rpcutils.UserType) (role rpcutils.UserType, nodeIntId int64, err error) {
// 默认包含大部分节点
if len(roles) == 0 {
roles = []rpcutils.UserType{rpcutils.UserTypeNode, rpcutils.UserTypeCluster, rpcutils.UserTypeAdmin, rpcutils.UserTypeUser, rpcutils.UserTypeDNS, rpcutils.UserTypeReport, rpcutils.UserTypeLog, rpcutils.UserTypeAPI}
roles = []rpcutils.UserType{rpcutils.UserTypeNode, rpcutils.UserTypeCluster, rpcutils.UserTypeAdmin, rpcutils.UserTypeUser, rpcutils.UserTypeDNS, rpcutils.UserTypeHTTPDNS, rpcutils.UserTypeReport, rpcutils.UserTypeLog, rpcutils.UserTypeAPI}
}
if ctx == nil {
@@ -191,6 +197,8 @@ func (this *BaseService) ValidateNodeId(ctx context.Context, roles ...rpcutils.U
nodeIntId = 0
case rpcutils.UserTypeDNS:
nodeIntId, err = models.SharedNSNodeDAO.FindEnabledNodeIdWithUniqueId(nil, nodeId)
case rpcutils.UserTypeHTTPDNS:
nodeIntId, err = models.SharedHTTPDNSNodeDAO.FindEnabledNodeIdWithUniqueId(nil, nodeId)
case rpcutils.UserTypeReport:
nodeIntId, err = models.SharedReportNodeDAO.FindEnabledNodeIdWithUniqueId(nil, nodeId)
case rpcutils.UserTypeAuthority:

View File

@@ -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

View File

@@ -19,7 +19,7 @@ type NodeTaskService struct {
// FindNodeTasks 获取单节点同步任务
func (this *NodeTaskService) FindNodeTasks(ctx context.Context, req *pb.FindNodeTasksRequest) (*pb.FindNodeTasksResponse, error) {
nodeType, nodeId, err := this.ValidateNodeId(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS)
nodeType, nodeId, err := this.ValidateNodeId(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS, rpcutils.UserTypeHTTPDNS)
if err != nil {
return nil, err
}
@@ -65,7 +65,7 @@ func (this *NodeTaskService) FindNodeTasks(ctx context.Context, req *pb.FindNode
// ReportNodeTaskDone 报告同步任务结果
func (this *NodeTaskService) ReportNodeTaskDone(ctx context.Context, req *pb.ReportNodeTaskDoneRequest) (*pb.RPCSuccess, error) {
_, _, err := this.ValidateNodeId(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS)
_, _, err := this.ValidateNodeId(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS, rpcutils.UserTypeHTTPDNS)
if err != nil {
return nil, err
}

View File

@@ -17,7 +17,7 @@ type NodeValueService struct {
// CreateNodeValue 记录数据
func (this *NodeValueService) CreateNodeValue(ctx context.Context, req *pb.CreateNodeValueRequest) (*pb.RPCSuccess, error) {
role, nodeId, _, err := rpcutils.ValidateRequest(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS, rpcutils.UserTypeUser)
role, nodeId, _, err := rpcutils.ValidateRequest(ctx, rpcutils.UserTypeNode, rpcutils.UserTypeDNS, rpcutils.UserTypeUser, rpcutils.UserTypeHTTPDNS)
if err != nil {
return nil, err
}
@@ -30,6 +30,8 @@ func (this *NodeValueService) CreateNodeValue(ctx context.Context, req *pb.Creat
clusterId, err = models.SharedNodeDAO.FindNodeClusterId(tx, nodeId)
case rpcutils.UserTypeDNS:
clusterId, err = models.SharedNSNodeDAO.FindNodeClusterId(tx, nodeId)
case rpcutils.UserTypeHTTPDNS:
clusterId, err = models.SharedHTTPDNSNodeDAO.FindNodeClusterId(tx, nodeId)
case rpcutils.UserTypeUser:
}
if err != nil {

View File

@@ -71,7 +71,7 @@ func (this *UserService) UpdateUser(ctx context.Context, req *pb.UpdateUserReque
return nil, err
}
err = models.SharedUserDAO.UpdateUser(tx, req.UserId, req.Username, req.Password, req.Fullname, req.Mobile, req.Tel, req.Email, req.Remark, req.IsOn, req.NodeClusterId, req.BandwidthAlgo)
err = models.SharedUserDAO.UpdateUser(tx, req.UserId, req.Username, req.Password, req.Fullname, req.Mobile, req.Tel, req.Email, req.Remark, req.IsOn, req.NodeClusterId, req.BandwidthAlgo, req.HttpdnsClusterIdsJSON)
if err != nil {
return nil, err
}
@@ -242,6 +242,20 @@ func (this *UserService) FindEnabledUser(ctx context.Context, req *pb.FindEnable
}
}
// 用户功能列表
var pbFeatures []*pb.UserFeature
userFeatures, err := models.SharedUserDAO.FindUserFeatures(tx, req.UserId)
if err != nil {
return nil, err
}
for _, f := range userFeatures {
pbFeatures = append(pbFeatures, &pb.UserFeature{
Name: f.Name,
Code: f.Code,
Description: f.Description,
})
}
return &pb.FindEnabledUserResponse{
User: &pb.User{
Id: int64(user.Id),
@@ -265,6 +279,8 @@ func (this *UserService) FindEnabledUser(ctx context.Context, req *pb.FindEnable
BandwidthAlgo: user.BandwidthAlgo,
OtpLogin: pbOtpAuth,
Lang: user.Lang,
Features: pbFeatures,
HttpdnsClusterIdsJSON: user.HttpdnsClusterIds,
},
}, nil
}

View File

@@ -5,6 +5,7 @@ package users
import (
"context"
"encoding/json"
"errors"
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
@@ -142,13 +143,24 @@ func (this *UserService) RegisterUser(ctx context.Context, req *pb.RegisterUserR
return errors.New("the username exists already")
}
features := registerConfig.Features
// 创建用户
userId, err := models.SharedUserDAO.CreateUser(tx, req.Username, req.Password, req.Fullname, req.Mobile, "", req.Email, "", req.Source, registerConfig.ClusterId, registerConfig.Features, req.Ip, !registerConfig.RequireVerification)
userId, err := models.SharedUserDAO.CreateUser(tx, req.Username, req.Password, req.Fullname, req.Mobile, "", req.Email, "", req.Source, registerConfig.ClusterId, features, req.Ip, !registerConfig.RequireVerification)
if err != nil {
return err
}
createdUserId = userId
// 自动关联默认 HTTPDNS 集群
if registerConfig.HTTPDNSIsOn && len(registerConfig.HTTPDNSDefaultClusterIds) > 0 {
httpdnsJSON, _ := json.Marshal(registerConfig.HTTPDNSDefaultClusterIds)
err = models.SharedUserDAO.UpdateUserHttpdnsClusterIds(tx, userId, httpdnsJSON)
if err != nil {
return err
}
}
// 发送激活邮件
if len(req.Email) > 0 && registerConfig.EmailVerification.IsOn {
_, err := models.SharedUserEmailVerificationDAO.CreateVerification(tx, userId, req.Email)

View File

@@ -16,6 +16,7 @@ const (
UserTypeCluster = "cluster"
UserTypeStat = "stat"
UserTypeDNS = "dns"
UserTypeHTTPDNS = "httpdns"
UserTypeLog = "log"
UserTypeAPI = "api"
UserTypeAuthority = "authority"

View File

@@ -142,6 +142,16 @@ func ValidateRequest(ctx context.Context, userTypes ...UserType) (userType UserT
return UserTypeUser, 0, 0, errors.New("context: not found node with id '" + nodeId + "'")
}
resultNodeId = nodeIntId
case UserTypeHTTPDNS:
nodeIntId, err := models.SharedHTTPDNSNodeDAO.FindEnabledNodeIdWithUniqueId(nil, nodeId)
if err != nil {
return UserTypeHTTPDNS, nodeIntId, 0, errors.New("context: " + err.Error())
}
if nodeIntId <= 0 {
return UserTypeHTTPDNS, nodeIntId, 0, errors.New("context: not found node with id '" + nodeId + "'")
}
nodeUserId = nodeIntId
resultNodeId = nodeIntId
}
if nodeUserId > 0 {

View File

@@ -171,6 +171,16 @@ func ValidateRequest(ctx context.Context, userTypes ...UserType) (userType UserT
}
nodeUserId = nodeIntId
resultNodeId = nodeIntId
case UserTypeHTTPDNS:
nodeIntId, err := models.SharedHTTPDNSNodeDAO.FindEnabledNodeIdWithUniqueId(nil, nodeId)
if err != nil {
return UserTypeHTTPDNS, nodeIntId, 0, errors.New("context: " + err.Error())
}
if nodeIntId <= 0 {
return UserTypeHTTPDNS, nodeIntId, 0, errors.New("context: not found node with id '" + nodeId + "'")
}
nodeUserId = nodeIntId
resultNodeId = nodeIntId
case UserTypeReport:
nodeIntId, err := models.SharedReportNodeDAO.FindEnabledNodeIdWithUniqueId(nil, nodeId)
if err != nil {

View File

@@ -0,0 +1,147 @@
package setup
import (
"context"
"strings"
"time"
"github.com/TeaOSLab/EdgeAPI/internal/clickhouse"
)
// EnsureClickHouseTables 自动确保日志相关 ClickHouse 表存在。
// 仅做 CREATE TABLE IF NOT EXISTS不会覆盖已有表结构。
func EnsureClickHouseTables() error {
client := clickhouse.NewClient()
if !client.IsConfigured() {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
sqls := []string{
`CREATE TABLE IF NOT EXISTS logs_ingest
(
timestamp DateTime CODEC(DoubleDelta, ZSTD(1)),
node_id UInt64,
cluster_id UInt64,
server_id UInt64,
host LowCardinality(String),
ip String,
method LowCardinality(String),
path String CODEC(ZSTD(1)),
status UInt16,
bytes_in UInt64 CODEC(Delta, ZSTD(1)),
bytes_out UInt64 CODEC(Delta, ZSTD(1)),
cost_ms UInt32 CODEC(Delta, ZSTD(1)),
ua String CODEC(ZSTD(1)),
referer String CODEC(ZSTD(1)),
log_type LowCardinality(String),
trace_id String,
firewall_policy_id UInt64 DEFAULT 0,
firewall_rule_group_id UInt64 DEFAULT 0,
firewall_rule_set_id UInt64 DEFAULT 0,
firewall_rule_id UInt64 DEFAULT 0,
request_headers String DEFAULT '' CODEC(ZSTD(3)),
request_body String DEFAULT '' CODEC(ZSTD(3)),
response_headers String DEFAULT '' CODEC(ZSTD(3)),
response_body String DEFAULT '' CODEC(ZSTD(3)),
attrs String DEFAULT '' CODEC(ZSTD(3)),
INDEX idx_trace_id trace_id TYPE bloom_filter(0.01) GRANULARITY 4,
INDEX idx_ip ip TYPE bloom_filter(0.01) GRANULARITY 4,
INDEX idx_host host TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4,
INDEX idx_fw_policy firewall_policy_id TYPE minmax GRANULARITY 4,
INDEX idx_status status TYPE minmax GRANULARITY 4
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (timestamp, node_id, server_id, trace_id)
SETTINGS index_granularity = 8192`,
`CREATE TABLE IF NOT EXISTS dns_logs_ingest
(
timestamp DateTime CODEC(DoubleDelta, ZSTD(1)),
request_id String,
node_id UInt64,
cluster_id UInt64,
domain_id UInt64,
record_id UInt64,
remote_addr String,
question_name String,
question_type LowCardinality(String),
record_name String,
record_type LowCardinality(String),
record_value String,
networking LowCardinality(String),
is_recursive UInt8,
error String CODEC(ZSTD(1)),
ns_route_codes Array(String),
content_json String DEFAULT '' CODEC(ZSTD(3)),
INDEX idx_request_id request_id TYPE bloom_filter(0.01) GRANULARITY 4,
INDEX idx_remote_addr remote_addr TYPE bloom_filter(0.01) GRANULARITY 4,
INDEX idx_question_name question_name TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4,
INDEX idx_domain_id domain_id TYPE minmax GRANULARITY 4
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (timestamp, request_id, node_id)
SETTINGS index_granularity = 8192`,
`CREATE TABLE IF NOT EXISTS httpdns_access_logs_ingest
(
request_id String,
cluster_id UInt64,
node_id UInt64,
app_id String,
app_name String,
domain String,
qtype LowCardinality(String),
client_ip String,
client_region String,
carrier String,
sdk_version String,
os LowCardinality(String),
result_ips String,
status LowCardinality(String),
error_code String,
cost_ms UInt32,
created_at UInt64,
day String,
summary String CODEC(ZSTD(1)),
INDEX idx_request_id request_id TYPE bloom_filter(0.01) GRANULARITY 4,
INDEX idx_cluster_id cluster_id TYPE minmax GRANULARITY 4,
INDEX idx_node_id node_id TYPE minmax GRANULARITY 4,
INDEX idx_app_id app_id TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4,
INDEX idx_domain domain TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4,
INDEX idx_status status TYPE minmax GRANULARITY 4
)
ENGINE = MergeTree
PARTITION BY day
ORDER BY (day, created_at, request_id, node_id)
SETTINGS index_granularity = 8192`,
}
for _, sql := range sqls {
stmt := strings.TrimSpace(sql)
if len(stmt) == 0 {
continue
}
if err := client.Execute(ctx, stmt); err != nil {
return err
}
}
// v1.5.1: 为 logs_ingest 添加 attrs 列(存储 cache.status 等扩展属性)
upgradeSQLs := []string{
`ALTER TABLE logs_ingest ADD COLUMN IF NOT EXISTS attrs String DEFAULT '' CODEC(ZSTD(3)) AFTER response_body`,
}
for _, sql := range upgradeSQLs {
stmt := strings.TrimSpace(sql)
if len(stmt) == 0 {
continue
}
if err := client.Execute(ctx, stmt); err != nil {
return err
}
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ import (
func TestSQLDump_Dump(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -53,7 +53,7 @@ func TestSQLDump_Dump(t *testing.T) {
func TestSQLDump_Apply(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {

View File

@@ -9,7 +9,7 @@ func TestSQLExecutor_Run(t *testing.T) {
var executor = NewSQLExecutor(&dbs.DBConfig{
Driver: "mysql",
Prefix: "edge",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge_new?charset=utf8mb4&multiStatements=true",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge_new?charset=utf8mb4&multiStatements=true",
})
err := executor.Run(false)
if err != nil {
@@ -22,7 +22,7 @@ func TestSQLExecutor_checkCluster(t *testing.T) {
var executor = NewSQLExecutor(&dbs.DBConfig{
Driver: "mysql",
Prefix: "edge",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge_new?charset=utf8mb4&multiStatements=true",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge_new?charset=utf8mb4&multiStatements=true",
})
db, err := dbs.NewInstanceFromConfig(executor.dbConfig)
if err != nil {
@@ -43,7 +43,7 @@ func TestSQLExecutor_checkMetricItems(t *testing.T) {
var executor = NewSQLExecutor(&dbs.DBConfig{
Driver: "mysql",
Prefix: "edge",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge_new?charset=utf8mb4&multiStatements=true",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge_new?charset=utf8mb4&multiStatements=true",
})
db, err := dbs.NewInstanceFromConfig(executor.dbConfig)
if err != nil {
@@ -64,7 +64,7 @@ func TestSQLExecutor_checkNS(t *testing.T) {
var executor = NewSQLExecutor(&dbs.DBConfig{
Driver: "mysql",
Prefix: "edge",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge_new?charset=utf8mb4&multiStatements=true",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge_new?charset=utf8mb4&multiStatements=true",
})
db, err := dbs.NewInstanceFromConfig(executor.dbConfig)
if err != nil {
@@ -85,7 +85,7 @@ func TestSQLExecutor_checkClientAgents(t *testing.T) {
var executor = NewSQLExecutor(&dbs.DBConfig{
Driver: "mysql",
Prefix: "edge",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&multiStatements=true",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&multiStatements=true",
})
db, err := dbs.NewInstanceFromConfig(executor.dbConfig)
if err != nil {

View File

@@ -110,6 +110,15 @@ var upgradeFuncs = []*upgradeVersion{
{
"1.4.4", upgradeV1_4_4,
},
{
"1.4.8", upgradeV1_4_8,
},
{
"1.4.9", upgradeV1_4_9,
},
{
"1.5.1", upgradeV1_5_1,
},
}
// UpgradeSQLData 升级SQL数据
@@ -269,14 +278,14 @@ func upgradeV0_0_10(db *dbs.DB) error {
// v0.2.5
func upgradeV0_2_5(db *dbs.DB) error {
// 更新用户
// 鏇存柊鐢ㄦ埛
_, err := db.Exec("UPDATE edgeUsers SET day=FROM_UNIXTIME(createdAt,'%Y%m%d') WHERE day IS NULL OR LENGTH(day)=0")
if err != nil {
return err
}
// 更新防火墙规则
ones, _, err := db.FindOnes("SELECT id, actions, action, actionOptions FROM edgeHTTPFirewallRuleSets WHERE actions IS NULL OR LENGTH(actions)=0")
// 更新防火墙规则
ones, _, err := db.FindOnes("SELECT id, actions, action, actionOptions FROM edgeHTTPFirewallRuleSets WHERE actions IS NULL OR LENGTH(actions)=0")
if err != nil {
return err
}
@@ -309,8 +318,8 @@ func upgradeV0_2_5(db *dbs.DB) error {
// v0.3.0
func upgradeV0_3_0(db *dbs.DB) error {
// 升级健康检查
ones, _, err := db.FindOnes("SELECT id,healthCheck FROM edgeNodeClusters WHERE state=1")
// 升级健康检查
ones, _, err := db.FindOnes("SELECT id,healthCheck FROM edgeNodeClusters WHERE state=1")
if err != nil {
return err
}
@@ -342,11 +351,10 @@ func upgradeV0_3_0(db *dbs.DB) error {
// v0.3.1
func upgradeV0_3_1(db *dbs.DB) error {
// 清空域名统计,已使用分表代替
// 因为可能有权限问题,所以我们忽略错误
_, _ = db.Exec("TRUNCATE table edgeServerDomainHourlyStats")
// 娓呯┖鍩熷悕缁熻锛屽凡浣跨敤鍒嗚〃浠f浛
// 鍥犱负鍙兘鏈夋潈闄愰棶棰橈紝鎵€浠ユ垜浠拷鐣ラ敊璇? _, _ = db.Exec("TRUNCATE table edgeServerDomainHourlyStats")
// 升级APIToken
// 鍗囩骇APIToken
ones, _, err := db.FindOnes("SELECT uniqueId,secret FROM edgeNodeClusters")
if err != nil {
return err
@@ -374,9 +382,9 @@ func upgradeV0_3_2(db *dbs.DB) error {
// gzip => compression
type HTTPGzipRef struct {
IsPrior bool `yaml:"isPrior" json:"isPrior"` // 是否覆盖
IsOn bool `yaml:"isOn" json:"isOn"` // 是否开启
GzipId int64 `yaml:"gzipId" json:"gzipId"` // 使用的配置ID
IsPrior bool `yaml:"isPrior" json:"isPrior"` // 鏄惁瑕嗙洊
IsOn bool `yaml:"isOn" json:"isOn"` // 是否开启
GzipId int64 `yaml:"gzipId" json:"gzipId"` // 使用的配置ID
}
webOnes, _, err := db.FindOnes("SELECT id, gzip FROM edgeHTTPWebs WHERE gzip IS NOT NULL AND compression IS NULL")
@@ -458,7 +466,7 @@ func upgradeV0_3_2(db *dbs.DB) error {
}
}
// 更新服务端口
// 鏇存柊鏈嶅姟绔彛
var serverDAO = models.NewServerDAO()
ones, err := serverDAO.Query(nil).
ResultPk().
@@ -479,14 +487,14 @@ func upgradeV0_3_2(db *dbs.DB) error {
// v0.3.3
func upgradeV0_3_3(db *dbs.DB) error {
// 升级CC请求数Code
_, err := db.Exec("UPDATE edgeHTTPFirewallRuleSets SET code='8002' WHERE name='CC请求数' AND code='8001'")
// 鍗囩骇CC璇锋眰鏁癈ode
_, err := db.Exec("UPDATE edgeHTTPFirewallRuleSets SET code='8002' WHERE name='CC璇锋眰鏁? AND code='8001'")
if err != nil {
return err
}
// 清除节点
// 删除7天以前的info日志
// 娓呴櫎鑺傜偣
// 鍒犻櫎7澶╀互鍓嶇殑info鏃ュ織
err = models.NewNodeLogDAO().DeleteExpiredLogsWithLevel(nil, "info", 7)
if err != nil {
return err
@@ -497,13 +505,13 @@ func upgradeV0_3_3(db *dbs.DB) error {
// v0.3.7
func upgradeV0_3_7(db *dbs.DB) error {
// 修改所有edgeNodeGrants中的su为0
// 淇敼鎵€鏈塭dgeNodeGrants涓殑su涓?
_, err := db.Exec("UPDATE edgeNodeGrants SET su=0 WHERE su=1")
if err != nil {
return err
}
// WAF预置分组
// WAF棰勭疆鍒嗙粍
_, err = db.Exec("UPDATE edgeHTTPFirewallRuleGroups SET isTemplate=1 WHERE LENGTH(code)>0")
if err != nil {
return err
@@ -514,7 +522,7 @@ func upgradeV0_3_7(db *dbs.DB) error {
// v0.4.0
func upgradeV0_4_0(db *dbs.DB) error {
// 升级SYN Flood配置
// 鍗囩骇SYN Flood閰嶇疆
synFloodJSON, err := json.Marshal(firewallconfigs.NewSYNFloodConfig())
if err == nil {
_, err := db.Exec("UPDATE edgeHTTPFirewallPolicies SET synFlood=? WHERE synFlood IS NULL AND state=1", string(synFloodJSON))
@@ -528,13 +536,13 @@ func upgradeV0_4_0(db *dbs.DB) error {
// v0.4.1
func upgradeV0_4_1(db *dbs.DB) error {
// 升级 servers.lastUserPlanId
// 鍗囩骇 servers.lastUserPlanId
_, err := db.Exec("UPDATE edgeServers SET lastUserPlanId=userPlanId WHERE userPlanId>0")
if err != nil {
return err
}
// 执行域名统计清理
// 鎵ц鍩熷悕缁熻娓呯悊
err = stats.NewServerDomainHourlyStatDAO().CleanDays(nil, 7)
if err != nil {
return err
@@ -545,7 +553,7 @@ func upgradeV0_4_1(db *dbs.DB) error {
// v0.4.5
func upgradeV0_4_5(db *dbs.DB) error {
// 升级访问日志自动分表
// 鍗囩骇璁块棶鏃ュ織鑷姩鍒嗚〃
{
var dao = models.NewSysSettingDAO()
valueJSON, err := dao.ReadSetting(nil, systemconfigs.SettingCodeAccessLogQueue)
@@ -569,7 +577,7 @@ func upgradeV0_4_5(db *dbs.DB) error {
}
}
// 升级一个防SQL注入规则
// 鍗囩骇涓€涓槻SQL娉ㄥ叆瑙勫垯
{
ones, _, err := db.FindOnes(`SELECT id FROM edgeHTTPFirewallRules WHERE value=?`, "(updatexml|extractvalue|ascii|ord|char|chr|count|concat|rand|floor|substr|length|len|user|database|benchmark|analyse)\\s*\\(")
if err != nil {
@@ -589,7 +597,7 @@ func upgradeV0_4_5(db *dbs.DB) error {
// v0.4.7
func upgradeV0_4_7(db *dbs.DB) error {
// 升级 edgeServers 中的 plainServerNames
// 鍗囩骇 edgeServers 涓殑 plainServerNames
{
ones, _, err := db.FindOnes("SELECT id,serverNames FROM edgeServers WHERE state=1")
if err != nil {
@@ -621,7 +629,7 @@ func upgradeV0_4_7(db *dbs.DB) error {
// v0.4.8
func upgradeV0_4_8(db *dbs.DB) error {
// 设置edgeIPLists中的serverId
// 璁剧疆edgeIPLists涓殑serverId
{
firewallPolicyOnes, _, err := db.FindOnes("SELECT inbound,serverId FROM edgeHTTPFirewallPolicies WHERE serverId>0")
if err != nil {
@@ -673,7 +681,7 @@ func upgradeV0_4_8(db *dbs.DB) error {
// v0.4.11
func upgradeV0_4_11(db *dbs.DB) error {
// 升级ns端口
// 鍗囩骇ns绔彛
{
// TCP
{
@@ -751,19 +759,19 @@ func upgradeV1_2_1(db *dbs.DB) error {
// 1.2.10
func upgradeV1_2_10(db *dbs.DB) error {
{
type OldGlobalConfig struct {
// HTTP & HTTPS相关配置
HTTPAll struct {
DomainAuditingIsOn bool `yaml:"domainAuditingIsOn" json:"domainAuditingIsOn"` // 域名是否需要审核
DomainAuditingPrompt string `yaml:"domainAuditingPrompt" json:"domainAuditingPrompt"` // 域名审核提示
} `yaml:"httpAll" json:"httpAll"`
type OldGlobalConfig struct {
// HTTP & HTTPS鐩稿叧閰嶇疆
HTTPAll struct {
DomainAuditingIsOn bool `yaml:"domainAuditingIsOn" json:"domainAuditingIsOn"` // 域名是否需要审核
DomainAuditingPrompt string `yaml:"domainAuditingPrompt" json:"domainAuditingPrompt"` // 域名审核提示
} `yaml:"httpAll" json:"httpAll"`
TCPAll struct {
PortRangeMin int `yaml:"portRangeMin" json:"portRangeMin"` // 最小端口
PortRangeMax int `yaml:"portRangeMax" json:"portRangeMax"` // 最大端口
DenyPorts []int `yaml:"denyPorts" json:"denyPorts"` // 禁止使用的端口
} `yaml:"tcpAll" json:"tcpAll"`
}
TCPAll struct {
PortRangeMin int `yaml:"portRangeMin" json:"portRangeMin"` // 最小端口
PortRangeMax int `yaml:"portRangeMax" json:"portRangeMax"` // 最大端口
DenyPorts []int `yaml:"denyPorts" json:"denyPorts"` // 禁止端口
} `yaml:"tcpAll" json:"tcpAll"`
}
globalConfigValue, err := db.FindCol(0, "SELECT value FROM edgeSysSettings WHERE code='serverGlobalConfig'")
if err != nil {
@@ -1023,7 +1031,7 @@ func upgradeV1_3_2(db *dbs.DB) error {
}
}
err = addRuleToGroup(ruleGroup.GetInt64("id"), "7010", "SQL注入检测", []*firewallconfigs.HTTPFirewallActionConfig{
err = addRuleToGroup(ruleGroup.GetInt64("id"), "7010", "SQL注入检测", []*firewallconfigs.HTTPFirewallActionConfig{
{
Code: firewallconfigs.HTTPFirewallActionPage,
Options: maps.Map{"status": 403, "body": ""},
@@ -1131,7 +1139,7 @@ func upgradeV1_3_2(db *dbs.DB) error {
}
}
err = addRuleToGroup(ruleGroup.GetInt64("id"), "1010", "XSS攻击检测", []*firewallconfigs.HTTPFirewallActionConfig{
err = addRuleToGroup(ruleGroup.GetInt64("id"), "1010", "XSS攻击检测", []*firewallconfigs.HTTPFirewallActionConfig{
{
Code: firewallconfigs.HTTPFirewallActionPage,
Options: maps.Map{"status": 403, "body": ""},
@@ -1233,8 +1241,8 @@ func upgradeV1_3_4(db *dbs.DB) error {
// 1.4.4
func upgradeV1_4_4(db *dbs.DB) error {
// 检查 encryption 字段是否已存在
col, err := db.FindCol(0, "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='edgeHTTPWebs' AND COLUMN_NAME='encryption'")
// 检查 encryption 字段是否已存在
col, err := db.FindCol(0, "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='edgeHTTPWebs' AND COLUMN_NAME='encryption'")
if err != nil {
return err
}
@@ -1253,3 +1261,79 @@ func upgradeV1_4_4(db *dbs.DB) error {
return nil
}
// 1.4.8
func upgradeV1_4_8(db *dbs.DB) error {
err := createHTTPDNSTables(db)
if err != nil {
return err
}
// edgeUsers: 增加 httpdnsClusterIds 字段
_, alterErr := db.Exec("ALTER TABLE `edgeUsers` ADD COLUMN `httpdnsClusterIds` text DEFAULT NULL")
if alterErr != nil {
if strings.Contains(alterErr.Error(), "Duplicate column") {
return nil
}
return alterErr
}
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
}
// v1.5.1
func upgradeV1_5_1(db *dbs.DB) error {
// 补充 httpdnsClusterIds 字段1.4.8 升级可能因版本号跳过未执行)
_, err := db.Exec("ALTER TABLE `edgeUsers` ADD COLUMN `httpdnsClusterIds` text DEFAULT NULL COMMENT 'HTTPDNS关联集群ID列表'")
if err != nil {
if strings.Contains(err.Error(), "Duplicate column") {
// 字段已存在,忽略
} else {
return err
}
}
// 确保 HTTPDNS 相关表存在
err = createHTTPDNSTables(db)
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 '/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应用域名表应用绑定的业务域名'",
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSCustomRules` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`appId` bigint unsigned DEFAULT '0',`domainId` bigint unsigned DEFAULT '0',`ruleName` varchar(255) DEFAULT NULL,`lineScope` varchar(64) DEFAULT NULL,`lineCarrier` varchar(64) DEFAULT NULL,`lineRegion` varchar(64) DEFAULT NULL,`lineProvince` varchar(64) DEFAULT NULL,`lineContinent` varchar(64) DEFAULT NULL,`lineCountry` varchar(128) DEFAULT NULL,`ttl` int unsigned DEFAULT '30',`isOn` tinyint unsigned DEFAULT '1',`priority` int unsigned DEFAULT '0',`updatedAt` bigint unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),KEY `domainId_isOn_priority` (`domainId`,`isOn`,`priority`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS自定义解析规则表按线路/地域匹配)'",
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSCustomRuleRecords` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`ruleId` bigint unsigned DEFAULT '0',`recordType` varchar(32) DEFAULT NULL,`recordValue` varchar(255) DEFAULT NULL,`weight` int unsigned DEFAULT '0',`sort` int unsigned DEFAULT '0',`state` tinyint unsigned DEFAULT '1',PRIMARY KEY (`id`),KEY `ruleId` (`ruleId`),KEY `state` (`state`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS自定义规则记录值表A/AAAA及权重'",
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSAccessLogs` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`requestId` varchar(128) DEFAULT NULL,`clusterId` bigint unsigned DEFAULT '0',`nodeId` bigint unsigned DEFAULT '0',`appId` varchar(64) DEFAULT NULL,`appName` varchar(255) DEFAULT NULL,`domain` varchar(255) DEFAULT NULL,`qtype` varchar(16) DEFAULT NULL,`clientIP` varchar(64) DEFAULT NULL,`clientRegion` varchar(255) DEFAULT NULL,`carrier` varchar(128) DEFAULT NULL,`sdkVersion` varchar(64) DEFAULT NULL,`os` varchar(64) DEFAULT NULL,`resultIPs` text,`status` varchar(32) DEFAULT NULL,`errorCode` varchar(64) DEFAULT NULL,`costMs` int unsigned DEFAULT '0',`createdAt` bigint unsigned DEFAULT '0',`day` varchar(8) DEFAULT NULL,`summary` text,PRIMARY KEY (`id`),UNIQUE KEY `requestId_nodeId` (`requestId`,`nodeId`),KEY `day_cluster_node_domain_status_createdAt` (`day`,`clusterId`,`nodeId`,`domain`,`status`,`createdAt`),KEY `appId` (`appId`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS访问日志表解析请求与结果'",
"CREATE TABLE IF NOT EXISTS `edgeHTTPDNSRuntimeLogs` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`clusterId` bigint unsigned DEFAULT '0',`nodeId` bigint unsigned DEFAULT '0',`level` varchar(32) DEFAULT NULL,`type` varchar(64) DEFAULT NULL,`module` varchar(64) DEFAULT NULL,`description` text,`count` bigint unsigned DEFAULT '1',`requestId` varchar(128) DEFAULT NULL,`createdAt` bigint unsigned DEFAULT '0',`day` varchar(8) DEFAULT NULL,PRIMARY KEY (`id`),KEY `day_cluster_node_level_createdAt` (`day`,`clusterId`,`nodeId`,`level`,`createdAt`),KEY `requestId` (`requestId`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HTTPDNS运行日志表节点运行与异常日志'",
}
for _, sql := range sqls {
if _, err := db.Exec(sql); err != nil {
return err
}
}
return nil
}

View File

@@ -11,7 +11,7 @@ import (
func TestUpgradeSQLData_v0_5_6(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -31,7 +31,7 @@ func TestUpgradeSQLData_v0_5_6(t *testing.T) {
func TestUpgradeSQLData_v1_3_4(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {

View File

@@ -10,7 +10,7 @@ import (
func TestUpgradeSQLData_v0_5_6(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -29,7 +29,7 @@ func TestUpgradeSQLData_v0_5_6(t *testing.T) {
func TestUpgradeSQLData_v0_5_8(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -48,7 +48,7 @@ func TestUpgradeSQLData_v0_5_8(t *testing.T) {
func TestUpgradeSQLData_v1_2_9(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {

View File

@@ -8,7 +8,7 @@ import (
func TestUpgradeSQLData(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge_new?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge_new?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -27,7 +27,7 @@ func TestUpgradeSQLData(t *testing.T) {
func TestUpgradeSQLData_v0_3_1(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge_new?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge_new?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -46,7 +46,7 @@ func TestUpgradeSQLData_v0_3_1(t *testing.T) {
func TestUpgradeSQLData_v0_3_2(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -65,7 +65,7 @@ func TestUpgradeSQLData_v0_3_2(t *testing.T) {
func TestUpgradeSQLData_v0_3_3(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -84,7 +84,7 @@ func TestUpgradeSQLData_v0_3_3(t *testing.T) {
func TestUpgradeSQLData_v0_3_7(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -103,7 +103,7 @@ func TestUpgradeSQLData_v0_3_7(t *testing.T) {
func TestUpgradeSQLData_v0_4_0(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -122,7 +122,7 @@ func TestUpgradeSQLData_v0_4_0(t *testing.T) {
func TestUpgradeSQLData_v0_4_1(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -141,7 +141,7 @@ func TestUpgradeSQLData_v0_4_1(t *testing.T) {
func TestUpgradeSQLData_v0_4_5(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -160,7 +160,7 @@ func TestUpgradeSQLData_v0_4_5(t *testing.T) {
func TestUpgradeSQLData_v0_4_7(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -179,7 +179,7 @@ func TestUpgradeSQLData_v0_4_7(t *testing.T) {
func TestUpgradeSQLData_v0_4_8(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -198,7 +198,7 @@ func TestUpgradeSQLData_v0_4_8(t *testing.T) {
func TestUpgradeSQLData_v0_4_9(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -217,7 +217,7 @@ func TestUpgradeSQLData_v0_4_9(t *testing.T) {
func TestUpgradeSQLData_v0_4_11(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -236,7 +236,7 @@ func TestUpgradeSQLData_v0_4_11(t *testing.T) {
func TestUpgradeSQLData_v0_5_3(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -255,7 +255,7 @@ func TestUpgradeSQLData_v0_5_3(t *testing.T) {
func TestUpgradeSQLData_v1_2_1(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -274,7 +274,7 @@ func TestUpgradeSQLData_v1_2_1(t *testing.T) {
func TestUpgradeSQLData_v1_2_10(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {
@@ -293,7 +293,7 @@ func TestUpgradeSQLData_v1_2_10(t *testing.T) {
func TestUpgradeSQLData_v1_3_2(t *testing.T) {
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
Driver: "mysql",
Dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s",
Dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s",
Prefix: "edge",
})
if err != nil {

View 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
}

View 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 == 0 || cluster.AutoRemoteStart == 0 {
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
}

View File

@@ -46,7 +46,7 @@ func (this *NodeTaskExtractor) Loop() error {
// 这里不解锁是为了让任务N秒钟之内只运行一次
for _, role := range []string{nodeconfigs.NodeRoleNode, nodeconfigs.NodeRoleDNS} {
for _, role := range []string{nodeconfigs.NodeRoleNode, nodeconfigs.NodeRoleDNS, nodeconfigs.NodeRoleHTTPDNS} {
err := models.SharedNodeTaskDAO.ExtractAllClusterTasks(nil, role)
if err != nil {
return err

View File

@@ -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")
@@ -23,7 +47,7 @@ function build() {
# checking environment
echo "checking required commands ..."
commands=("zip" "unzip" "go" "find" "sed")
commands=("zip" "unzip" "go" "find" "sed" "protoc")
for cmd in "${commands[@]}"; do
if [ "$(which "${cmd}")" ]; then
echo "checking ${cmd}: ok"
@@ -41,6 +65,19 @@ function build() {
ZIP="${NAME}-${OS}-${ARCH}-${TAG}-v${VERSION}.zip"
fi
# regenerate protobuf files to keep rawDesc in sync with .proto
echo "regenerating protobuf files ..."
EDGE_COMMON_BUILD="$ROOT/../../EdgeCommon/build"
if [ -f "$EDGE_COMMON_BUILD/build.sh" ]; then
(cd "$EDGE_COMMON_BUILD" && ./build.sh)
if [ $? -ne 0 ]; then
echo "protobuf generation failed! Fix errors before building."
exit 1
fi
else
echo "EdgeCommon/build/build.sh not found, skipping protobuf generation"
fi
# build edge-api
APINodeVersion=$(lookup-version "$ROOT""/../../EdgeAPI/internal/const/const.go")
echo "building edge-api v${APINodeVersion} ..."
@@ -58,7 +95,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,14 +106,14 @@ 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
mkdir "$DIST"
mkdir "$DIST"/bin
mkdir "$DIST"/configs
mkdir "$DIST"/logs
fi
rm -rf "$DIST"
mkdir -p "$DIST"/bin
mkdir -p "$DIST"/configs
mkdir -p "$DIST"/logs
cp -R "$ROOT"/../web "$DIST"/
rm -f "$DIST"/web/tmp/*
@@ -109,30 +146,6 @@ function build() {
rm -f "$(basename "$EDGE_API_ZIP_FILE")"
# ensure edge-api package always contains fluent-bit runtime assets/packages
FLUENT_ROOT="$ROOT/../../deploy/fluent-bit"
FLUENT_DIST="$DIST/edge-api/deploy/fluent-bit"
if [ -d "$FLUENT_ROOT" ]; then
verify_fluent_bit_package_matrix "$FLUENT_ROOT" "$ARCH" || exit 1
rm -rf "$FLUENT_DIST"
mkdir -p "$FLUENT_DIST"
FLUENT_FILES=(
"parsers.conf"
)
for file in "${FLUENT_FILES[@]}"; do
if [ -f "$FLUENT_ROOT/$file" ]; then
cp "$FLUENT_ROOT/$file" "$FLUENT_DIST/"
fi
done
if [ -d "$FLUENT_ROOT/packages" ]; then
cp -R "$FLUENT_ROOT/packages" "$FLUENT_DIST/"
fi
rm -f "$FLUENT_DIST/.gitignore"
rm -f "$FLUENT_DIST"/logs.db*
rm -rf "$FLUENT_DIST/storage"
fi
# 鍒犻櫎 MaxMind 鏁版嵁搴撴枃浠讹紙浣跨敤宓屽叆鐨勬暟鎹簱锛屼笉闇€瑕佸閮ㄦ枃浠讹級
find . -name "*.mmdb" -type f -delete
@@ -194,39 +207,6 @@ function build() {
echo "[done]"
}
function verify_fluent_bit_package_matrix() {
FLUENT_ROOT=$1
ARCH=$2
REQUIRED_FILES=()
if [ "$ARCH" = "amd64" ]; then
REQUIRED_FILES=(
"packages/linux-amd64/fluent-bit_4.2.2_amd64.deb"
"packages/linux-amd64/fluent-bit-4.2.2-1.x86_64.rpm"
)
elif [ "$ARCH" = "arm64" ]; then
REQUIRED_FILES=(
"packages/linux-arm64/fluent-bit_4.2.2_arm64.deb"
"packages/linux-arm64/fluent-bit-4.2.2-1.aarch64.rpm"
)
else
echo "[error] unsupported arch for fluent-bit package validation: $ARCH"
return 1
fi
MISSING=0
for FILE in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$FLUENT_ROOT/$FILE" ]; then
echo "[error] fluent-bit matrix package missing: $FLUENT_ROOT/$FILE"
MISSING=1
fi
done
if [ "$MISSING" -ne 0 ]; then
return 1
fi
return 0
}
function lookup-version() {
FILE=$1
VERSION_DATA=$(cat "$FILE")

View File

@@ -5,7 +5,7 @@ default:
dbs:
prod:
driver: "mysql"
dsn: "root:123456@tcp(127.0.0.1:3306)/db_edge?charset=utf8mb4&timeout=30s"
dsn: "root:123456@tcp(127.0.0.1:3308)/db_edge?charset=utf8mb4&timeout=30s"
prefix: "edge"
models:
package: internal/web/models

View File

@@ -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"

View File

@@ -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
}
}

View File

@@ -10,7 +10,7 @@ require (
github.com/TeaOSLab/EdgeCommon v0.0.0-00010101000000-000000000000
github.com/TeaOSLab/EdgePlus v0.0.0-00010101000000-000000000000
github.com/cespare/xxhash/v2 v2.3.0
github.com/go-sql-driver/mysql v1.5.0
github.com/go-sql-driver/mysql v1.7.0
github.com/iwind/TeaGo v0.0.0-20240429060313-31a7bc8e9cc9
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4
github.com/miekg/dns v1.1.43

View File

@@ -1,3 +1,9 @@
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/TeaOSLab/EdgeAPI v1.3.9 h1:qUSQJsDmxb6XBatu2tKMZ24+6hmJHhBBGx5WXBTbfN4=
github.com/TeaOSLab/EdgeAPI v1.3.9/go.mod h1:/XK/wF+DcEeQ0v42VpWC/co5sRUuExbs7wvTNv6b8EE=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -7,14 +13,16 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/frankban/quicktest v1.5.0/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -34,8 +42,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/iwind/TeaGo v0.0.0-20240429060313-31a7bc8e9cc9 h1:lqSd+OeAMzPlejoVtsmHJEeQ6/MWXCr+Ws2eX9/Rewg=
github.com/iwind/TeaGo v0.0.0-20240429060313-31a7bc8e9cc9/go.mod h1:SfqVbWyIPdVflyA6lMgicZzsoGS8pyeLiTRe8/CIpGI=
github.com/iwind/TeaGo v0.0.0-20240508072741-7647e70b7070 h1:0YHZBcuXYbvtQ0XfEdtzr/XybiMrwD8vV1lvgAwzUW4=
github.com/iwind/TeaGo v0.0.0-20240508072741-7647e70b7070/go.mod h1:SfqVbWyIPdVflyA6lMgicZzsoGS8pyeLiTRe8/CIpGI=
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4 h1:VWGsCqTzObdlbf7UUE3oceIpcEKi4C/YBUszQXk118A=
github.com/iwind/gosock v0.0.0-20211103081026-ee4652210ca4/go.mod h1:H5Q7SXwbx3a97ecJkaS2sD77gspzE7HFUafBO0peEyA=
github.com/iwind/gosock v0.0.0-20220505115348-f88412125a62 h1:HJH6RDheAY156DnIfJSD/bEvqyXzsZuE2gzs8PuUjoo=
github.com/iwind/gosock v0.0.0-20220505115348-f88412125a62/go.mod h1:H5Q7SXwbx3a97ecJkaS2sD77gspzE7HFUafBO0peEyA=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@@ -43,11 +55,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/mozillazg/go-pinyin v0.18.0 h1:hQompXO23/0ohH8YNjvfsAITnCQImCiR/Fny8EhIeW0=
github.com/mozillazg/go-pinyin v0.18.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
@@ -81,6 +98,8 @@ github.com/shirou/gopsutil/v3 v3.22.5 h1:atX36I/IXgFiB81687vSiBI5zrMsxcIBkP9cQMJ
github.com/shirou/gopsutil/v3 v3.22.5/go.mod h1:so9G9VzeHt/hsd0YwqprnjHnfARAUktauykSbr+y2gA=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -122,24 +141,18 @@ golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=

View File

@@ -18,6 +18,7 @@ const (
AdminModuleCodeServer AdminModuleCode = "server" // 网站
AdminModuleCodeNode AdminModuleCode = "node" // 节点
AdminModuleCodeDNS AdminModuleCode = "dns" // DNS
AdminModuleCodeHttpDNS AdminModuleCode = "httpdns" // HTTPDNS
AdminModuleCodeNS AdminModuleCode = "ns" // 域名服务
AdminModuleCodeAdmin AdminModuleCode = "admin" // 系统用户
AdminModuleCodeUser AdminModuleCode = "user" // 平台用户
@@ -106,7 +107,19 @@ func AllowModule(adminId int64, module string) bool {
list, ok := sharedAdminModuleMapping[adminId]
if ok {
return list.Allow(module)
if list.Allow(module) {
return true
}
// Backward compatibility: old admin module sets may not contain "httpdns".
// In that case, reuse related CDN module permissions to keep HTTPDNS visible/accessible.
if module == AdminModuleCodeHttpDNS {
return list.Allow(AdminModuleCodeDNS) ||
list.Allow(AdminModuleCodeNode) ||
list.Allow(AdminModuleCodeServer)
}
return false
}
return false
@@ -226,6 +239,11 @@ func AllModuleMaps(langCode string) []maps.Map {
"code": AdminModuleCodeDNS,
"url": "/dns",
},
{
"name": "HTTPDNS",
"code": AdminModuleCodeHttpDNS,
"url": "/httpdns/clusters",
},
}
if teaconst.IsPlus {
m = append(m, maps.Map{

View 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
}

View File

@@ -1,9 +1,9 @@
package teaconst
const (
Version = "1.4.7" //1.3.9
Version = "1.5.1" //1.3.9
APINodeVersion = "1.4.7" //1.3.9
APINodeVersion = "1.5.1" //1.3.9
ProductName = "Edge Admin"
ProcessName = "edge-admin"

Some files were not shown because too many files have changed in this diff Show More