Phase 0+1+2 续: 项目骨架 + 配置 + 日志 + 存储层
Phase 0 (项目骨架):
- main.go 改为最小骨架 (config + logging + gin + /healthz + 优雅退出)
- internal/{config,logging,storage,cluster,...}/ 目录占位
Phase 1 (配置 + 日志):
- internal/config: env 解析 + 必填校验 + token 隐藏的 String()
- internal/logging: slog multi-handler 双输出 (终端 text + 主文件 JSON)
+ StartToolCall per-tool 独立文件 (0600), tools 目录 0700
Phase 2 续 (存储层):
- internal/cluster: 16 字段 Cluster struct (AuthPassword json:"-")
- internal/storage: modernc.org/sqlite 接入, WAL 模式, schema 自动迁移
- ClusterRepo CRUD: Create/Get/List/Update/Delete + ErrNotFound
+ AuthPassword 空字符串 = 保留旧密码 (核心约定)
- 7 个单元测试全绿 (:memory: DB)
- created_at/updated_at 改纳秒精度, 消除 List 测试的 sleep 特殊 case
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// Schema defines the SQLite schema for clusters and audit_log.
|
||||
const Schema = `CREATE TABLE IF NOT EXISTS clusters (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
rm_url TEXT NOT NULL,
|
||||
shs_url TEXT NOT NULL,
|
||||
spark_submit_execute_bin TEXT NOT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
auth_type TEXT NOT NULL DEFAULT 'none',
|
||||
auth_username TEXT,
|
||||
auth_password_enc BLOB,
|
||||
ssl_verify INTEGER NOT NULL DEFAULT 1,
|
||||
ssl_ca_bundle TEXT,
|
||||
url_allowlist TEXT,
|
||||
default_submit_args TEXT,
|
||||
rate_limit_per_min INTEGER NOT NULL DEFAULT 10,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp INTEGER NOT NULL,
|
||||
actor TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
cluster_id TEXT,
|
||||
details TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit_log(timestamp DESC);
|
||||
`
|
||||
|
||||
// DB is the storage handle.
|
||||
type DB struct {
|
||||
Path string
|
||||
sqlDB *sql.DB
|
||||
}
|
||||
|
||||
// Open opens the SQLite database at path, creating the parent directory if
|
||||
// needed. It enables WAL mode and foreign keys, then applies the schema.
|
||||
func Open(path string) (*DB, error) {
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("storage: mkdir %s: %w", dir, err)
|
||||
}
|
||||
|
||||
sqlDB, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("storage: open sqlite %s: %w", path, err)
|
||||
}
|
||||
|
||||
if _, err := sqlDB.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
||||
_ = sqlDB.Close()
|
||||
return nil, fmt.Errorf("storage: set WAL mode: %w", err)
|
||||
}
|
||||
if _, err := sqlDB.Exec("PRAGMA foreign_keys=ON"); err != nil {
|
||||
_ = sqlDB.Close()
|
||||
return nil, fmt.Errorf("storage: enable foreign keys: %w", err)
|
||||
}
|
||||
|
||||
d := &DB{Path: path, sqlDB: sqlDB}
|
||||
if err := d.Migrate(); err != nil {
|
||||
_ = sqlDB.Close()
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Close releases the underlying database connection.
|
||||
func (d *DB) Close() error {
|
||||
if d == nil || d.sqlDB == nil {
|
||||
return nil
|
||||
}
|
||||
return d.sqlDB.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user