feat: 18 引擎驱动全家桶 + 驱动管理/离线包 + 设置中心 + 工作台交互打磨
驱动接入(全部本机真实例验收): - Elasticsearch 原生 SQL API、InfluxDB 原生 HTTP+Flux(官方 JDBC 付费许可,实测弃用) - ZooKeeper/MongoDB JVM agent(连接时自动下载官方驱动 jar,sha256 钉版本到 ~/.opsxdb/drivers/) - 达梦 JDBC agent 桥就绪(待厂商 jar+实例);TiDB 真实例验收 - 协议族代码审查放行 PolarDB/TDSQL/OceanBase/Kingbase/GaussDB/Oracle - 驱动能力模型:按驱动开关对象树分组/右键菜单/行编辑,NoSQL 树拍平(对象直挂库节点) 驱动管理: - 驱动管理弹框(状态/预装/离线包导入/JDK 检测)+ /api/drivers/* 三接口 - opsxdb-driver 驱动商店仓库(清单/fetch.sh/pack.sh 离线包/import.sh) - 连接时懒加载:DMG 零 Java 产物,首连自动下载(实测 ZK 九件套 1.35s),离线包自动回退 UI/交互: - 设置中心(⚙):常规/执行/结果/驱动/快捷键/关于,左导航+右面板(对标 opsxshell) - 实例级展开箭头(行首 SVG chevron,替代字体回退失真的 Unicode 三角) - 树内嵌套渲染修复:对象树挂在激活连接下;工具栏窄窗口不断行(按钮 nowrap+整颗换行) - 侧栏连接行精简(图标+连接名)、SQLite SSH 守卫、Oracle 死参数清理 质量: - Go 协议层/离线回退单测、E2E 门控测试(ES/ZK/Influx/Mongo/TiDB/JDBC 桥) - 驱动注册表单一事实来源 connection-types.json,README/BACKLOG/AGENTS 同步
This commit is contained in:
1 parent
26f459d1b8
commit
a9510235bb
156 files changed
+17200
-2520
No files matched your search
@@ -0,0 +1,379 @@
|
||||
package db
|
||||
|
||||
// agent_e2e_test.go —— JDBC agent 桥端到端验证(真实 JVM + 真实 JDBC 驱动)。
|
||||
// 前置:本机 JDK、agent jar(OPSXDB_AGENT_JAR 或默认 ~/.opsxdb/agents/jdbc/)。
|
||||
// 触发:OPSXDB_E2E_JDBC=1 才运行(CI 无 JVM 时自动跳过)。
|
||||
// 覆盖:StartAgent 全链路(gson 校验/JVM 启动/握手)→ open → DDL/DML → query → 元数据。
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestE2EAgentSQLiteBridge(t *testing.T) {
|
||||
if os.Getenv("OPSXDB_E2E_JDBC") != "1" {
|
||||
t.Skip("置 OPSXDB_E2E_JDBC=1 并备好 JDK 与 agent jar 后运行")
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
jars := filepath.Join(home, ".opsxdb", "agents", "jdbc", "drivers", "sqlite-e2e")
|
||||
if _, err := os.Stat(filepath.Join(jars, "sqlite-jdbc-3.46.1.3.jar")); err != nil {
|
||||
t.Skipf("缺 sqlite-jdbc jar: %s", jars)
|
||||
}
|
||||
t.Setenv("OPSXDB_E2E_JARS", jars)
|
||||
|
||||
spec := AgentSpec{
|
||||
DriverKey: "e2e-sqlite",
|
||||
DriverClass: "org.sqlite.JDBC",
|
||||
URLTemplate: "jdbc:sqlite:{{.Database}}",
|
||||
JarsDirEnv: "OPSXDB_E2E_JARS",
|
||||
}
|
||||
agentDriverSpecs[spec.DriverKey] = spec // 注册进表,AgentConfigured 才能按 JarsDirEnv 解析
|
||||
t.Cleanup(func() { delete(agentDriverSpecs, spec.DriverKey) })
|
||||
cli, err := StartAgent(spec)
|
||||
if err != nil {
|
||||
t.Fatalf("StartAgent: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { cli.Shutdown(context.Background()) })
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "e2e.db")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
handle := ""
|
||||
{
|
||||
var res struct {
|
||||
Handle string `json:"handle"`
|
||||
}
|
||||
if err := cli.call(ctx, "open", map[string]any{"url": "jdbc:sqlite:" + dbPath, "user": "", "password": ""}, &res); err != nil {
|
||||
t.Fatalf("open: %v (stderr=%s)", err, cli.stderrTail.String())
|
||||
}
|
||||
handle = res.Handle
|
||||
}
|
||||
mustExec := func(sql string) {
|
||||
t.Helper()
|
||||
var r struct {
|
||||
AffectedRows int64 `json:"affectedRows"`
|
||||
}
|
||||
if err := cli.call(ctx, "exec", map[string]any{"handle": handle, "sql": sql, "timeoutSec": 30}, &r); err != nil {
|
||||
t.Fatalf("exec %q: %v (stderr=%s)", sql, err, cli.stderrTail.String())
|
||||
}
|
||||
}
|
||||
mustExec("CREATE TABLE demo(id INTEGER PRIMARY KEY, name TEXT)")
|
||||
mustExec("INSERT INTO demo(name) VALUES ('opsx-e2e')")
|
||||
|
||||
{
|
||||
var res struct {
|
||||
Columns []string `json:"columns"`
|
||||
Rows []map[string]interface{} `json:"rows"`
|
||||
}
|
||||
if err := cli.call(ctx, "query", map[string]any{"handle": handle, "sql": "SELECT id, name FROM demo", "maxRows": 100, "timeoutSec": 30}, &res); err != nil {
|
||||
t.Fatalf("query: %v", err)
|
||||
}
|
||||
if len(res.Rows) != 1 || res.Rows[0]["name"] != "opsx-e2e" {
|
||||
t.Fatalf("rows = %+v", res.Rows)
|
||||
}
|
||||
}
|
||||
{
|
||||
var res struct {
|
||||
Names []string `json:"names"`
|
||||
}
|
||||
if err := cli.call(ctx, "metaTables", map[string]any{"handle": handle, "schema": ""}, &res); err != nil {
|
||||
t.Fatalf("metaTables: %v", err)
|
||||
}
|
||||
found := false
|
||||
for _, n := range res.Names {
|
||||
if n == "demo" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("metaTables 未包含 demo: %v", res.Names)
|
||||
}
|
||||
}
|
||||
{
|
||||
var res struct {
|
||||
Columns []ColumnInfo `json:"columns"`
|
||||
}
|
||||
if err := cli.call(ctx, "metaColumns", map[string]any{"handle": handle, "schema": "", "table": "demo"}, &res); err != nil {
|
||||
t.Fatalf("metaColumns: %v", err)
|
||||
}
|
||||
if len(res.Columns) != 2 {
|
||||
t.Fatalf("columns = %+v", res.Columns)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestE2EElasticsearch —— 真实 ES 8(docker,安全关闭)+ 官方 x-pack-sql-jdbc 走 agent 桥全链路。
|
||||
// 触发:OPSXDB_E2E_ES=1;前置 ES :9200 可连、~/.opsxdb/agents/jdbc/drivers/elasticsearch/ 有官方 JDBC jar。
|
||||
func TestE2EElasticsearch(t *testing.T) {
|
||||
if os.Getenv("OPSXDB_E2E_ES") != "1" {
|
||||
t.Skip("置 OPSXDB_E2E_ES=1 并启动 ES :9200 后运行")
|
||||
}
|
||||
// 种子文档(让 SHOW TABLES / SELECT 有真实对象)
|
||||
req, _ := http.NewRequest(http.MethodPut, "http://127.0.0.1:9200/e2e_smoke/_doc/1?refresh=true", strings.NewReader(`{"name":"opsx","n":42}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Skipf("ES :9200 不可达: %v", err)
|
||||
}
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
t.Fatalf("种子文档写入失败: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
c := ConnReq{Driver: "elasticsearch", Host: "127.0.0.1", Port: 9200}
|
||||
if err := TestConnection(c); err != nil {
|
||||
t.Fatalf("TestConnection: %v", err)
|
||||
}
|
||||
dbs, err := ListDatabases(context.Background(), c)
|
||||
if err != nil || len(dbs) != 1 || dbs[0] != "default" {
|
||||
t.Fatalf("ListDatabases = %v, %v", dbs, err)
|
||||
}
|
||||
cc := c
|
||||
cc.Database = "default"
|
||||
tables, err := ListTables(context.Background(), cc)
|
||||
if err != nil {
|
||||
t.Fatalf("ListTables: %v", err)
|
||||
}
|
||||
found := false
|
||||
for _, n := range tables {
|
||||
if n == "e2e_smoke" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("tables 未含 e2e_smoke: %v", tables)
|
||||
}
|
||||
r := ExecSQL(context.Background(), ConnReq{Driver: "elasticsearch", Host: "127.0.0.1", Port: 9200, Database: "default", SQL: "SELECT name, n FROM e2e_smoke", Limit: 100})
|
||||
if r.Error != "" {
|
||||
t.Fatalf("ExecSQL: %s (stderr 见测试日志)", r.Error)
|
||||
}
|
||||
if r.RowCount < 1 {
|
||||
t.Fatalf("rows = %+v", r.Rows)
|
||||
}
|
||||
}
|
||||
|
||||
// TestE2EZooKeeper —— 真实 ZK(docker)+ OpsxZkAgent 全链路:连接/浏览/读写/删除。
|
||||
// 触发:OPSXDB_E2E_ZK=1;前置 ZK :2181、~/.opsxdb/agents/jdbc/drivers/zookeeper/ 有官方 jar。
|
||||
func TestE2EZooKeeper(t *testing.T) {
|
||||
if os.Getenv("OPSXDB_E2E_ZK") != "1" {
|
||||
t.Skip("置 OPSXDB_E2E_ZK=1 并启动 ZK :2181 后运行")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
|
||||
c := ConnReq{Driver: "zookeeper", Host: "127.0.0.1", Port: 2181}
|
||||
if err := TestConnection(c); err != nil {
|
||||
t.Skipf("ZK :2181 不可达: %v", err)
|
||||
}
|
||||
dbs, err := ListDatabases(ctx, c)
|
||||
if err != nil || len(dbs) == 0 {
|
||||
t.Fatalf("ListDatabases = %v, %v", dbs, err)
|
||||
}
|
||||
hasZk := false
|
||||
for _, n := range dbs {
|
||||
if n == "zookeeper" {
|
||||
hasZk = true
|
||||
}
|
||||
}
|
||||
if !hasZk {
|
||||
t.Fatalf("根下应含 zookeeper: %v", dbs)
|
||||
}
|
||||
cc := c
|
||||
cc.Database = "zookeeper"
|
||||
tables, err := ListTables(ctx, cc)
|
||||
if err != nil {
|
||||
t.Fatalf("ListTables: %v", err)
|
||||
}
|
||||
hasConfig := false
|
||||
for _, n := range tables {
|
||||
if n == "config" {
|
||||
hasConfig = true
|
||||
}
|
||||
}
|
||||
if !hasConfig {
|
||||
t.Fatalf("/zookeeper 下应含 config: %v", tables)
|
||||
}
|
||||
// 读:GET
|
||||
r := ExecSQL(ctx, ConnReq{Driver: "zookeeper", Host: "127.0.0.1", Port: 2181, Database: "zookeeper", SQL: "GET /zookeeper/config", Limit: 100})
|
||||
if r.Error != "" {
|
||||
t.Fatalf("GET: %s", r.Error)
|
||||
}
|
||||
if r.RowCount != 1 || r.Rows[0]["path"] != "/zookeeper/config" {
|
||||
t.Fatalf("GET rows = %+v", r.Rows)
|
||||
}
|
||||
// 写:CREATE → GET → SET → DELETE(DELETE 无 WHERE 属危险语句,走确认通道)
|
||||
w := func(sql string) {
|
||||
t.Helper()
|
||||
res := ExecSQL(ctx, ConnReq{Driver: "zookeeper", Host: "127.0.0.1", Port: 2181, SQL: sql, ConfirmDanger: true})
|
||||
if res.Error != "" {
|
||||
t.Fatalf("%s → %s", sql, res.Error)
|
||||
}
|
||||
}
|
||||
w("CREATE /opsxe2e hello-opsx")
|
||||
r = ExecSQL(ctx, ConnReq{Driver: "zookeeper", Host: "127.0.0.1", Port: 2181, SQL: "GET /opsxe2e", Limit: 100})
|
||||
if r.Rows[0]["data"] != "hello-opsx" {
|
||||
t.Fatalf("CREATE 后 data = %v", r.Rows[0]["data"])
|
||||
}
|
||||
w("SET /opsxe2e updated")
|
||||
r = ExecSQL(ctx, ConnReq{Driver: "zookeeper", Host: "127.0.0.1", Port: 2181, SQL: "GET /opsxe2e", Limit: 100})
|
||||
if r.Rows[0]["data"] != "updated" {
|
||||
t.Fatalf("SET 后 data = %v", r.Rows[0]["data"])
|
||||
}
|
||||
// 危险语句未确认时必须先返回 NeedConfirm
|
||||
guard := ExecSQL(ctx, ConnReq{Driver: "zookeeper", Host: "127.0.0.1", Port: 2181, SQL: "DELETE /opsxe2e"})
|
||||
if !guard.NeedConfirm {
|
||||
t.Fatalf("DELETE 未确认应返回 NeedConfirm,got %+v", guard)
|
||||
}
|
||||
w("DELETE /opsxe2e")
|
||||
}
|
||||
|
||||
// TestE2EInfluxDB —— 真实 InfluxDB 2.7(docker)原生 HTTP 全链路:bucket/measurement/Flux 查询。
|
||||
// 触发:OPSXDB_E2E_INFLUX=1;前置 InfluxDB :8086(org opsx / token opsxdb-demo-token / bucket demo)。
|
||||
func TestE2EInfluxDB(t *testing.T) {
|
||||
if os.Getenv("OPSXDB_E2E_INFLUX") != "1" {
|
||||
t.Skip("置 OPSXDB_E2E_INFLUX=1 并启动 InfluxDB :8086 后运行")
|
||||
}
|
||||
// 种子点(行协议写入)
|
||||
wreq, _ := http.NewRequest(http.MethodPost, "http://127.0.0.1:8086/api/v2/write?org=opsx&bucket=demo&precision=s",
|
||||
strings.NewReader("e2e_m,site=local v=42"))
|
||||
wreq.Header.Set("Authorization", "Token opsxdb-demo-token")
|
||||
wreq.Header.Set("Content-Type", "text/plain; charset=utf-8")
|
||||
wresp, err := http.DefaultClient.Do(wreq)
|
||||
if err != nil {
|
||||
t.Skipf("InfluxDB :8086 不可达: %v", err)
|
||||
}
|
||||
_, _ = io.Copy(io.Discard, wresp.Body)
|
||||
wresp.Body.Close()
|
||||
if wresp.StatusCode >= 400 {
|
||||
t.Fatalf("种子写入失败: HTTP %d", wresp.StatusCode)
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond) // 存储引擎落盘
|
||||
|
||||
c := ConnReq{Driver: "influxdb", Host: "127.0.0.1", Port: 8086, User: "opsx", Password: "opsxdb-demo-token"}
|
||||
if err := TestConnection(c); err != nil {
|
||||
t.Fatalf("TestConnection: %v", err)
|
||||
}
|
||||
dbs, err := ListDatabases(context.Background(), c)
|
||||
if err != nil {
|
||||
t.Fatalf("ListDatabases: %v", err)
|
||||
}
|
||||
hasDemo := false
|
||||
for _, n := range dbs {
|
||||
if n == "demo" {
|
||||
hasDemo = true
|
||||
}
|
||||
}
|
||||
if !hasDemo {
|
||||
t.Fatalf("buckets 未含 demo: %v", dbs)
|
||||
}
|
||||
cc := c
|
||||
cc.Database = "demo"
|
||||
tables, err := ListTables(context.Background(), cc)
|
||||
if err != nil {
|
||||
t.Fatalf("ListTables: %v", err)
|
||||
}
|
||||
hasM := false
|
||||
for _, n := range tables {
|
||||
if n == "e2e_m" {
|
||||
hasM = true
|
||||
}
|
||||
}
|
||||
if !hasM {
|
||||
t.Fatalf("measurements 未含 e2e_m: %v", tables)
|
||||
}
|
||||
r := ExecSQL(context.Background(), ConnReq{
|
||||
Driver: "influxdb", Host: "127.0.0.1", Port: 8086, User: "opsx", Password: "opsxdb-demo-token",
|
||||
SQL: `from(bucket: "demo") |> range(start: -1h) |> filter(fn: (r) => r._measurement == "e2e_m")`,
|
||||
Limit: 100,
|
||||
})
|
||||
if r.Error != "" {
|
||||
t.Fatalf("Flux 查询: %s", r.Error)
|
||||
}
|
||||
if r.RowCount < 1 {
|
||||
t.Fatalf("rows = %+v", r.Rows)
|
||||
}
|
||||
}
|
||||
|
||||
// TestE2EMongoDB —— 真实 MongoDB 7 + OpsxMongoAgent 全链路:库/集合浏览、DSL 查改增删、危险确认。
|
||||
// 触发:OPSXDB_E2E_MONGO=1;前置 MongoDB :27017、~/.opsxdb/agents/jdbc/drivers/mongodb/ 有官方 jar。
|
||||
func TestE2EMongoDB(t *testing.T) {
|
||||
if os.Getenv("OPSXDB_E2E_MONGO") != "1" {
|
||||
t.Skip("置 OPSXDB_E2E_MONGO=1 并启动 MongoDB :27017 后运行")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
|
||||
c := ConnReq{Driver: "mongodb", Host: "127.0.0.1", Port: 27017}
|
||||
if err := TestConnection(c); err != nil {
|
||||
t.Skipf("MongoDB :27017 不可达: %v", err)
|
||||
}
|
||||
// 种子:插入文档(确认通道)
|
||||
seed := ExecSQL(ctx, ConnReq{Driver: "mongodb", Host: "127.0.0.1", Port: 27017, Database: "opsxe2e", SQL: `db.c1.insertOne({"name":"opsx","n":7})`, ConfirmDanger: false})
|
||||
if seed.Error != "" {
|
||||
t.Fatalf("insertOne: %s", seed.Error)
|
||||
}
|
||||
dbs, err := ListDatabases(ctx, c)
|
||||
if err != nil || len(dbs) == 0 {
|
||||
t.Fatalf("ListDatabases = %v, %v", dbs, err)
|
||||
}
|
||||
hasDb := false
|
||||
for _, n := range dbs {
|
||||
if n == "opsxe2e" {
|
||||
hasDb = true
|
||||
}
|
||||
}
|
||||
if !hasDb {
|
||||
t.Fatalf("库列表未含 opsxe2e: %v", dbs)
|
||||
}
|
||||
cc := c
|
||||
cc.Database = "opsxe2e"
|
||||
tables, err := ListTables(ctx, cc)
|
||||
if err != nil || len(tables) == 0 {
|
||||
t.Fatalf("ListTables = %v, %v", tables, err)
|
||||
}
|
||||
found := false
|
||||
for _, n := range tables {
|
||||
if n == "c1" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("集合列表未含 c1: %v", tables)
|
||||
}
|
||||
cols, err := ListColumns(ctx, ConnReq{Driver: "mongodb", Host: "127.0.0.1", Port: 27017, Database: "opsxe2e", Table: "c1"})
|
||||
if err != nil || len(cols) == 0 {
|
||||
t.Fatalf("ListColumns = %v, %v", cols, err)
|
||||
}
|
||||
// 查询
|
||||
q := ExecSQL(ctx, ConnReq{Driver: "mongodb", Host: "127.0.0.1", Port: 27017, Database: "opsxe2e", SQL: `db.c1.find({"name":"opsx"})`, Limit: 100})
|
||||
if q.Error != "" || q.RowCount < 1 {
|
||||
t.Fatalf("find: err=%s rows=%+v", q.Error, q.Rows)
|
||||
}
|
||||
// 危险确认:deleteMany 未确认必须先返回 NeedConfirm
|
||||
guard := ExecSQL(ctx, ConnReq{Driver: "mongodb", Host: "127.0.0.1", Port: 27017, Database: "opsxe2e", SQL: `db.c1.deleteMany({"name":"opsx"})`})
|
||||
if !guard.NeedConfirm {
|
||||
t.Fatalf("deleteMany 未确认应 NeedConfirm,got %+v", guard)
|
||||
}
|
||||
// 更新 + 删除(确认通道)
|
||||
u := ExecSQL(ctx, ConnReq{Driver: "mongodb", Host: "127.0.0.1", Port: 27017, Database: "opsxe2e", SQL: `db.c1.updateOne({"name":"opsx"}, {"$set":{"n":8}})`, ConfirmDanger: true})
|
||||
if u.Error != "" {
|
||||
t.Fatalf("updateOne: %s", u.Error)
|
||||
}
|
||||
d := ExecSQL(ctx, ConnReq{Driver: "mongodb", Host: "127.0.0.1", Port: 27017, Database: "opsxe2e", SQL: `db.c1.deleteMany({"name":"opsx"})`, ConfirmDanger: true})
|
||||
if d.Error != "" {
|
||||
t.Fatalf("deleteMany: %s", d.Error)
|
||||
}
|
||||
cnt := ExecSQL(ctx, ConnReq{Driver: "mongodb", Host: "127.0.0.1", Port: 27017, Database: "opsxe2e", SQL: `db.c1.count({"name":"opsx"})`})
|
||||
if fmt.Sprintf("%v", cnt.Rows[0]["n"]) != "0" {
|
||||
t.Fatalf("count after delete = %v", cnt.Rows[0]["n"])
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user