实现本地三节点 K3s 同构环境、脱敏生产快照、Gemini 图片和多参考图视频协议模拟、统一验收报告及故障注入。\n\n新增 Worker 容量控制器、资源与连接预算、任务恢复保护,并将生产验收拆分为 validation 执行和人工 CAS 放量。\n\n验证包括 Go 全量测试、PostgreSQL HTTP 集成测试、go vet、OpenAPI、ShellCheck、前端检查、迁移及发布脚本测试。
196 lines
5.1 KiB
Go
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
|
|
}
|