5e1482e349
build / build (push) Successful in 1m40s
根因:生产可恢复性只有 PRODUCTION_BACKUP_RESTORE_VERIFIED 这个手填 env 开关, 页面没有任何入口去真跑一次恢复;CLI(librarydrill)又不是 worker 能复用的实现, 所以"已验证"既证明不了跑过、也证明不了跑的是当前目标。 改动: - 共享实现 internal/libraryverify(维护隔离→捕获→基线校验→canary→恢复→复核→关窗口), cmd/librarydrill 改为薄封装,CLI 与 worker 只允许一份执行体。 - verify 成为独立业务作业:kind + VerifyStages(capture/verify/canary/restore/finalize), 与 sync/restore/baseline 共用同一把写互斥;迁移 79 加 kind 约束、重建部分唯一索引、 新建 library_restore_verifications(按 target_id 绑定,换库/索引/桶旧证据自动失效, 同 target+scope+job 幂等)。 - worker:admitLibraryJob + runOwnedLibraryAttempt 之后调用共享实现,阶段实时写 library_job_stages,结束后写真实终态;通过写 passed 证据,失败写 failed+原因(不伪成功); 中断接管按同一作业 id 收掉维护窗口。 - API:POST/GET /api/v1/library/verifications(管理员;闸口关闭/维护中拒绝;返回完整 整库目标、最近证据、失败原因),OpenAPI 同步。 - 闸口:同步/恢复/续作的证据判断改为读当前目标证据;历史 env 仅在证据缺失时兜底, 证据存在时以证据为准。 - 页面:生产保护与恢复面板加「恢复验证」块(完整 KB/索引/桶、状态、最近证据、失败原因、 管理员按钮、5s 轮询),作业类型补 verify 文案。 测试:make verify 16/16 全绿(go 1387 例 + web 454 例 + 契约/迁移/密钥/依赖锁定); 新增用例覆盖证据四条件、闸口证据优先、页面无证据/失败/通过三种呈现与管理员可见性。
99 lines
4.1 KiB
Go
99 lines
4.1 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"knowledge-governance/internal/governance"
|
|
)
|
|
|
|
// 恢复验证证据:只由 worker 在真实演练结束后写入;这里只做落库与按当前目标读取。
|
|
//
|
|
// 读取永远带 target_id = 当前部署目标:换库/换索引/换桶之后旧证据自动失效,
|
|
// 不需要任何人去"记得把开关关掉"。
|
|
|
|
// SaveLibraryRestoreVerification 幂等登记一次演练结果(同一 target+scope+job 只留一条)。
|
|
func (s *Store) SaveLibraryRestoreVerification(ctx context.Context, verification governance.LibraryRestoreVerification) error {
|
|
if verification.ID == "" || verification.JobID == "" {
|
|
return fmt.Errorf("restore verification requires id and job id")
|
|
}
|
|
if verification.Scope != "internal" && verification.Scope != "external" {
|
|
return fmt.Errorf("restore verification scope %q is invalid", verification.Scope)
|
|
}
|
|
if verification.Status != governance.LibraryVerificationPassed && verification.Status != governance.LibraryVerificationFailed {
|
|
return fmt.Errorf("restore verification status %q is invalid", verification.Status)
|
|
}
|
|
report, err := json.Marshal(verification.Report)
|
|
if err != nil {
|
|
return fmt.Errorf("encode restore verification report: %w", err)
|
|
}
|
|
_, err = s.db.Exec(ctx, `
|
|
INSERT INTO library_restore_verifications (id, target_id, scope, status, snapshot_id, job_id, failure, report, started_at, finished_at)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
|
|
ON CONFLICT (target_id, scope, job_id) DO UPDATE
|
|
SET status = EXCLUDED.status, snapshot_id = EXCLUDED.snapshot_id, failure = EXCLUDED.failure,
|
|
report = EXCLUDED.report, started_at = EXCLUDED.started_at, finished_at = EXCLUDED.finished_at`,
|
|
verification.ID, s.libraryTargetID, verification.Scope, verification.Status,
|
|
verification.SnapshotID, verification.JobID, verification.Failure, report,
|
|
verification.StartedAt.UTC(), verification.FinishedAt.UTC())
|
|
if err != nil {
|
|
return fmt.Errorf("save restore verification: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LatestLibraryRestoreVerification 按完成时间取当前目标该 scope 的最近一条证据。
|
|
// 没有记录返回 (zero,false,nil):那不是错误,页面据此说"还没有验证过"。
|
|
func (s *Store) LatestLibraryRestoreVerification(ctx context.Context, scope string) (governance.LibraryRestoreVerification, bool, error) {
|
|
var (
|
|
item governance.LibraryRestoreVerification
|
|
report []byte
|
|
)
|
|
err := s.db.QueryRow(ctx, `
|
|
SELECT id, scope, status, snapshot_id, job_id, failure, report, started_at, finished_at
|
|
FROM library_restore_verifications
|
|
WHERE target_id=$1 AND scope=$2
|
|
ORDER BY finished_at DESC, created_at DESC
|
|
LIMIT 1`, s.libraryTargetID, scope).Scan(&item.ID, &item.Scope, &item.Status, &item.SnapshotID,
|
|
&item.JobID, &item.Failure, &report, &item.StartedAt, &item.FinishedAt)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return governance.LibraryRestoreVerification{}, false, nil
|
|
}
|
|
return governance.LibraryRestoreVerification{}, false, fmt.Errorf("load restore verification: %w", err)
|
|
}
|
|
item.Report = decodeEvidenceObject(report)
|
|
return item, true, nil
|
|
}
|
|
|
|
// LibraryRestoreVerifications 是页面用的轻量清单(最近 N 条),按目标过滤。
|
|
func (s *Store) LibraryRestoreVerifications(ctx context.Context, scope string, limit int) ([]governance.LibraryRestoreVerification, error) {
|
|
if limit <= 0 {
|
|
limit = 10
|
|
}
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT id, scope, status, snapshot_id, job_id, failure, started_at, finished_at
|
|
FROM library_restore_verifications
|
|
WHERE target_id=$1 AND scope=$2
|
|
ORDER BY finished_at DESC, created_at DESC
|
|
LIMIT $3`, s.libraryTargetID, scope, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list restore verifications: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
items := make([]governance.LibraryRestoreVerification, 0, limit)
|
|
for rows.Next() {
|
|
var item governance.LibraryRestoreVerification
|
|
if err := rows.Scan(&item.ID, &item.Scope, &item.Status, &item.SnapshotID, &item.JobID,
|
|
&item.Failure, &item.StartedAt, &item.FinishedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, rows.Err()
|
|
}
|