mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
7d71f1e4e1
- New internal/repository/logstore abstraction: exported domain interfaces (AccessLogStore/ObservabilityStore/UserAccessLogStore/StatusStore), config-driven provider (Active/Build/Migrating/SetConfigReader), GORM implementation for PostgreSQL/SQLite (incl. hourly rollups computed in real time, migration listers, PG partition maintenance), and a ClickHouse wrapper preserving the native batch path; repository facade delegates to logstore; import-lint test enforces apps never import analyticsrepo. - ClickHouse is now optional: the log DB is either the main DB (postgres when database.enabled, else sqlite) or clickhouse; boot validation + first-run seed; log_database / log_db_migration are protected keys. - New user task 切换日志数据库 (of_log_db_switch): freeze log writes, drain batch writers, copy all 6 raw log tables by id (preserving IDs) with target-partition pre-creation for PG, flip log_database on success, clear the freeze flag on failure. - Per-store retention (log_retention_days_*) with expiry cleanup folded into the daily system_cleanup task; legacy database_auto_cleanup_* and of_database_auto_cleanup decommissioned. - goose migrations: 6 log tables in PG (2 monthly-partitioned) + SQLite, retention config seeds, schedule cleanup; GET /api/v1/admin/status/log-database endpoint; frontend retention settings, switch-task UI and status badge; changelog and docs updated. docs(plan): log database decoupling implementation plan docs(design): log database decoupling design (ClickHouse optional)
198 lines
5.9 KiB
Go
198 lines
5.9 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package integration
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
|
|
ofnode "github.com/Rain-kl/Wavelet/internal/apps/openflare/node"
|
|
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
|
|
"github.com/Rain-kl/Wavelet/internal/model"
|
|
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/glebarez/sqlite"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func setupProtocolTestEnv(t *testing.T) (*gin.Engine, func()) {
|
|
t.Helper()
|
|
|
|
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
|
DisableForeignKeyConstraintWhenMigrating: true,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, sqliteDB.AutoMigrate(
|
|
&model.OpenFlareNode{},
|
|
&model.SystemConfig{},
|
|
&model.OpenFlareApplyLog{},
|
|
&model.OpenFlareNodeSystemProfile{},
|
|
&model.OpenFlareHealthEvent{},
|
|
&model.ConfigVersion{},
|
|
))
|
|
|
|
db.SetDB(sqliteDB)
|
|
agent.ResetAuthCacheForTest()
|
|
testhelper.SetupLogStoresForTest(t)
|
|
|
|
engine := testhelper.NewTestGinEngine()
|
|
mountOpenFlareTestRoutes(engine)
|
|
|
|
cleanup := func() {
|
|
db.SetDB(nil)
|
|
agent.ResetAuthCacheForTest()
|
|
}
|
|
return engine, cleanup
|
|
}
|
|
|
|
func TestAgentRelayFlaredProtocol(t *testing.T) {
|
|
engine, cleanup := setupProtocolTestEnv(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
t.Run("create edge node and heartbeat with X-Agent-Token", func(t *testing.T) {
|
|
edge, err := ofnode.CreateNode(ctx, ofnode.Input{
|
|
Name: "edge-1",
|
|
IP: "10.0.0.1",
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, edge.AccessToken)
|
|
assert.Equal(t, "edge_node", edge.NodeType)
|
|
|
|
rec := performJSONRequest(t, engine, http.MethodPost, "/api/v1/agent/nodes/heartbeat", map[string]any{
|
|
"name": "edge-1",
|
|
"ip": "203.0.113.10",
|
|
"version": "0.1.0",
|
|
}, map[string]string{
|
|
"X-Agent-Token": edge.AccessToken,
|
|
})
|
|
assert.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
resp := requireAPIOK(t, rec)
|
|
data := unmarshalAPIMap(t, resp.Data)
|
|
assert.NotNil(t, data["agent_settings"])
|
|
})
|
|
|
|
t.Run("create tunnel_relay node and relay heartbeat", func(t *testing.T) {
|
|
relayNode, err := ofnode.CreateNode(ctx, ofnode.Input{
|
|
Name: "relay-1",
|
|
NodeType: "tunnel_relay",
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, relayNode.AccessToken)
|
|
|
|
rec := performJSONRequest(t, engine, http.MethodPost, "/api/v1/relay/heartbeat", map[string]any{
|
|
"version": "v0.1.0",
|
|
"frp_version": "0.61.0",
|
|
"relay_status": "healthy",
|
|
"name": "relay-1",
|
|
"ip": "203.0.113.20",
|
|
}, map[string]string{
|
|
"X-Agent-Token": relayNode.AccessToken,
|
|
})
|
|
assert.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
resp := requireAPIOK(t, rec)
|
|
var heartbeatData struct {
|
|
RelayConfig map[string]any `json:"relay_config"`
|
|
RelaySettings map[string]any `json:"relay_settings"`
|
|
}
|
|
unmarshalAPIData(t, resp.Data, &heartbeatData)
|
|
assert.NotNil(t, heartbeatData.RelayConfig)
|
|
assert.NotNil(t, heartbeatData.RelaySettings)
|
|
|
|
stored, err := repository.GetOpenFlareNodeByNodeID(ctx, relayNode.NodeID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "online", stored.Status)
|
|
assert.Equal(t, "healthy", stored.RelayStatus)
|
|
})
|
|
|
|
t.Run("create tunnel_client node and flared heartbeat with X-Tunnel-Token", func(t *testing.T) {
|
|
clientNode, err := ofnode.CreateNode(ctx, ofnode.Input{
|
|
Name: "client-1",
|
|
NodeType: "tunnel_client",
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, clientNode.AccessToken)
|
|
|
|
rec := performJSONRequest(t, engine, http.MethodPost, "/api/v1/tunnel/heartbeat", map[string]any{
|
|
"client_version": "v0.2.0",
|
|
"frp_version": "0.61.0",
|
|
"tunnel_status": "running",
|
|
}, map[string]string{
|
|
"X-Tunnel-Token": clientNode.AccessToken,
|
|
})
|
|
assert.Equal(t, http.StatusOK, rec.Code)
|
|
requireAPIOK(t, rec)
|
|
|
|
stored, err := repository.GetOpenFlareNodeByNodeID(ctx, clientNode.NodeID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "online", stored.Status)
|
|
assert.Equal(t, "v0.2.0", stored.Version)
|
|
})
|
|
|
|
t.Run("agent register with discovery token from options", func(t *testing.T) {
|
|
bootstrap, err := ofnode.GetBootstrapToken(ctx)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, bootstrap.DiscoveryToken)
|
|
|
|
rec := performJSONRequest(t, engine, http.MethodPost, "/api/v1/agent/nodes/register", map[string]any{
|
|
"name": "discovered-edge",
|
|
"ip": "203.0.113.30",
|
|
"version": "0.2.0",
|
|
}, map[string]string{
|
|
"X-Agent-Token": bootstrap.DiscoveryToken,
|
|
})
|
|
assert.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
resp := requireAPIOK(t, rec)
|
|
var registration agent.RegistrationResponse
|
|
unmarshalAPIData(t, resp.Data, ®istration)
|
|
assert.NotEmpty(t, registration.NodeID)
|
|
assert.NotEmpty(t, registration.AccessToken)
|
|
assert.Equal(t, "discovered-edge", registration.Name)
|
|
|
|
stored, err := repository.GetOpenFlareNodeByNodeID(ctx, registration.NodeID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "online", stored.Status)
|
|
assert.Equal(t, registration.AccessToken, stored.AccessToken)
|
|
})
|
|
|
|
t.Run("POST agent apply-logs", func(t *testing.T) {
|
|
edge, err := ofnode.CreateNode(ctx, ofnode.Input{
|
|
Name: "edge-apply",
|
|
IP: "10.0.0.2",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
rec := performJSONRequest(t, engine, http.MethodPost, "/api/v1/agent/apply-logs", map[string]any{
|
|
"version": "20260618-001",
|
|
"result": "success",
|
|
"message": "apply ok",
|
|
}, map[string]string{
|
|
"X-Agent-Token": edge.AccessToken,
|
|
})
|
|
assert.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
resp := requireAPIOK(t, rec)
|
|
var applyLog model.OpenFlareApplyLog
|
|
unmarshalAPIData(t, resp.Data, &applyLog)
|
|
assert.Equal(t, edge.NodeID, applyLog.NodeID)
|
|
assert.Equal(t, "success", applyLog.Result)
|
|
assert.Equal(t, "20260618-001", applyLog.Version)
|
|
|
|
stored, err := repository.GetOpenFlareNodeByNodeID(ctx, edge.NodeID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "online", stored.Status)
|
|
assert.Equal(t, "20260618-001", stored.CurrentVersion)
|
|
})
|
|
}
|