Files
easyai-ai-gateway/apps/api/cmd/acceptance-snapshot/main.go
T
wangbo e05922b0f4 feat(acceptance): 建立同构验收与弹性容量体系
实现本地三节点 K3s 同构环境、脱敏生产快照、Gemini 图片和多参考图视频协议模拟、统一验收报告及故障注入。\n\n新增 Worker 容量控制器、资源与连接预算、任务恢复保护,并将生产验收拆分为 validation 执行和人工 CAS 放量。\n\n验证包括 Go 全量测试、PostgreSQL HTTP 集成测试、go vet、OpenAPI、ShellCheck、前端检查、迁移及发布脚本测试。
2026-07-31 18:02:24 +08:00

196 lines
5.1 KiB
Go

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
}