feat(acceptance): 建立同构验收与弹性容量体系
实现本地三节点 K3s 同构环境、脱敏生产快照、Gemini 图片和多参考图视频协议模拟、统一验收报告及故障注入。\n\n新增 Worker 容量控制器、资源与连接预算、任务恢复保护,并将生产验收拆分为 validation 执行和人工 CAS 放量。\n\n验证包括 Go 全量测试、PostgreSQL HTTP 集成测试、go vet、OpenAPI、ShellCheck、前端检查、迁移及发布脚本测试。
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/acceptancesnapshot"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
os.Exit(64)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
var err error
|
||||
switch os.Args[1] {
|
||||
case "export":
|
||||
err = runExport(ctx, os.Args[2:])
|
||||
case "validate":
|
||||
err = runValidate(os.Args[2:])
|
||||
case "import":
|
||||
err = runImport(ctx, os.Args[2:])
|
||||
default:
|
||||
usage()
|
||||
os.Exit(64)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "acceptance snapshot:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, `Usage:
|
||||
easyai-ai-gateway-acceptance-snapshot export --release-sha <SHA> --output <file>
|
||||
easyai-ai-gateway-acceptance-snapshot validate --input <file>
|
||||
easyai-ai-gateway-acceptance-snapshot import --input <file> --local-cluster-id <id>
|
||||
|
||||
export is read-only. import refuses to write unless system_settings contains a
|
||||
matching acceptance_local_cluster_id marker.`)
|
||||
}
|
||||
|
||||
func runExport(ctx context.Context, args []string) error {
|
||||
flags := flag.NewFlagSet("export", flag.ContinueOnError)
|
||||
releaseSHA := flags.String("release-sha", "", "full source release SHA")
|
||||
output := flags.String("output", "", "secret-safe snapshot output path")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if flags.NArg() != 0 {
|
||||
return errors.New("unexpected export arguments")
|
||||
}
|
||||
databaseURL := strings.TrimSpace(os.Getenv("AI_GATEWAY_DATABASE_URL"))
|
||||
if databaseURL == "" {
|
||||
return errors.New("AI_GATEWAY_DATABASE_URL is required")
|
||||
}
|
||||
outputPath, err := safeOutputPath(*output)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pool, err := pgxpool.New(ctx, databaseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pool.Close()
|
||||
snapshot, err := acceptancesnapshot.Export(ctx, pool, acceptancesnapshot.ExportOptions{
|
||||
ReleaseSHA: *releaseSHA,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload, err := acceptancesnapshot.Encode(snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(outputPath, payload, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(
|
||||
"acceptance_snapshot_export=PASS schema=%s config_hash=%s snapshot_sha256=%s\n",
|
||||
snapshot.SchemaVersion,
|
||||
snapshot.Source.ConfigHash,
|
||||
snapshot.SnapshotSHA256,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runValidate(args []string) error {
|
||||
flags := flag.NewFlagSet("validate", flag.ContinueOnError)
|
||||
input := flags.String("input", "", "snapshot input path")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if flags.NArg() != 0 {
|
||||
return errors.New("unexpected validate arguments")
|
||||
}
|
||||
snapshot, err := readSnapshot(*input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(
|
||||
"acceptance_snapshot_validate=PASS schema=%s release=%s config_hash=%s snapshot_sha256=%s\n",
|
||||
snapshot.SchemaVersion,
|
||||
snapshot.Source.ReleaseSHA,
|
||||
snapshot.Source.ConfigHash,
|
||||
snapshot.SnapshotSHA256,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runImport(ctx context.Context, args []string) error {
|
||||
flags := flag.NewFlagSet("import", flag.ContinueOnError)
|
||||
input := flags.String("input", "", "snapshot input path")
|
||||
clusterID := flags.String("local-cluster-id", "", "required local cluster marker")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if flags.NArg() != 0 {
|
||||
return errors.New("unexpected import arguments")
|
||||
}
|
||||
databaseURL := strings.TrimSpace(os.Getenv("AI_GATEWAY_DATABASE_URL"))
|
||||
if databaseURL == "" {
|
||||
return errors.New("AI_GATEWAY_DATABASE_URL is required")
|
||||
}
|
||||
snapshot, err := readSnapshot(*input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pool, err := pgxpool.New(ctx, databaseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pool.Close()
|
||||
if err := acceptancesnapshot.Import(ctx, pool, snapshot, *clusterID); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(
|
||||
"acceptance_snapshot_import=PASS cluster_id=%s config_hash=%s snapshot_sha256=%s\n",
|
||||
strings.TrimSpace(*clusterID),
|
||||
snapshot.Source.ConfigHash,
|
||||
snapshot.SnapshotSHA256,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func readSnapshot(path string) (acceptancesnapshot.Snapshot, error) {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return acceptancesnapshot.Snapshot{}, errors.New("snapshot path is required")
|
||||
}
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return acceptancesnapshot.Snapshot{}, err
|
||||
}
|
||||
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
||||
return acceptancesnapshot.Snapshot{}, errors.New("snapshot must be a regular non-symlink file")
|
||||
}
|
||||
payload, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return acceptancesnapshot.Snapshot{}, err
|
||||
}
|
||||
return acceptancesnapshot.Decode(payload)
|
||||
}
|
||||
|
||||
func safeOutputPath(path string) (string, error) {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return "", errors.New("output path is required")
|
||||
}
|
||||
absolute, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parent := filepath.Dir(absolute)
|
||||
if err := os.MkdirAll(parent, 0o700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if info, err := os.Lstat(absolute); err == nil {
|
||||
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
||||
return "", errors.New("output must be a regular non-symlink file")
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return "", err
|
||||
}
|
||||
return absolute, nil
|
||||
}
|
||||
Reference in New Issue
Block a user