feat(acceptance): 建立同构验收与弹性容量体系
实现本地三节点 K3s 同构环境、脱敏生产快照、Gemini 图片和多参考图视频协议模拟、统一验收报告及故障注入。\n\n新增 Worker 容量控制器、资源与连接预算、任务恢复保护,并将生产验收拆分为 validation 执行和人工 CAS 放量。\n\n验证包括 Go 全量测试、PostgreSQL HTTP 集成测试、go vet、OpenAPI、ShellCheck、前端检查、迁移及发布脚本测试。
This commit is contained in:
@@ -22,6 +22,7 @@ const SystemSettingGatewayTrafficMode = "gateway_traffic_mode"
|
||||
var (
|
||||
acceptanceReleaseSHAPattern = regexp.MustCompile(`^[0-9a-f]{40}$`)
|
||||
acceptanceDigestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
||||
capacityConfigHashPattern = regexp.MustCompile(`^[0-9a-f]{64}$`)
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -39,6 +40,7 @@ type GatewayTrafficMode struct {
|
||||
ReleaseSHA string `json:"releaseSha,omitempty"`
|
||||
APIImageDigest string `json:"apiImageDigest,omitempty"`
|
||||
WorkerImageDigest string `json:"workerImageDigest,omitempty"`
|
||||
ConfigHash string `json:"configHash,omitempty"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
@@ -84,11 +86,21 @@ type FinishAcceptanceRunInput struct {
|
||||
}
|
||||
|
||||
type PromoteAcceptanceRunInput struct {
|
||||
RunID string `json:"-"`
|
||||
RunID string `json:"-"`
|
||||
Revision int64 `json:"revision"`
|
||||
ReleaseSHA string `json:"releaseSha"`
|
||||
APIImageDigest string `json:"apiImageDigest"`
|
||||
WorkerImageDigest string `json:"workerImageDigest"`
|
||||
ConfigHash string `json:"configHash"`
|
||||
CapacityProfiles []CapacityProfileInput `json:"capacityProfiles"`
|
||||
}
|
||||
|
||||
type PauseGatewayTrafficInput struct {
|
||||
Revision int64 `json:"revision"`
|
||||
ReleaseSHA string `json:"releaseSha"`
|
||||
APIImageDigest string `json:"apiImageDigest"`
|
||||
WorkerImageDigest string `json:"workerImageDigest"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func DefaultGatewayTrafficMode() GatewayTrafficMode {
|
||||
@@ -250,9 +262,25 @@ func (s *Store) FinishAcceptanceRun(ctx context.Context, input FinishAcceptanceR
|
||||
UPDATE gateway_acceptance_runs
|
||||
SET status = $2, report = $3::jsonb, failure_reason = NULLIF($4, ''),
|
||||
finished_at = now(), updated_at = now()
|
||||
WHERE id = $1::uuid AND status = 'running'
|
||||
WHERE (
|
||||
id = $1::uuid
|
||||
AND status = 'running'
|
||||
)
|
||||
OR (
|
||||
id = $1::uuid
|
||||
AND $2::text = 'failed'
|
||||
AND status = 'passed'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM system_settings setting
|
||||
WHERE setting.setting_key = $5
|
||||
AND setting.value->>'mode' = 'validation'
|
||||
AND setting.value->>'runId' = $1::text
|
||||
)
|
||||
)
|
||||
RETURNING `+acceptanceRunColumns,
|
||||
strings.TrimSpace(input.RunID), status, report, strings.TrimSpace(input.FailureReason),
|
||||
SystemSettingGatewayTrafficMode,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -318,18 +346,37 @@ FOR UPDATE`, SystemSettingGatewayTrafficMode).Scan(&value); err != nil {
|
||||
current.WorkerImageDigest != strings.TrimSpace(input.WorkerImageDigest) {
|
||||
return GatewayTrafficMode{}, ErrAcceptanceStateConflict
|
||||
}
|
||||
var status string
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT status
|
||||
run, err := scanAcceptanceRun(tx.QueryRow(ctx, `
|
||||
SELECT `+acceptanceRunColumns+`
|
||||
FROM gateway_acceptance_runs
|
||||
WHERE id = $1::uuid
|
||||
FOR UPDATE`, current.RunID).Scan(&status); err != nil {
|
||||
FOR UPDATE`, current.RunID))
|
||||
if err != nil {
|
||||
return GatewayTrafficMode{}, err
|
||||
}
|
||||
if status != "passed" {
|
||||
if run.Status != "passed" {
|
||||
return GatewayTrafficMode{}, ErrAcceptancePromotionGates
|
||||
}
|
||||
next := GatewayTrafficMode{Mode: "live", Revision: current.Revision + 1}
|
||||
stagedConfigHash, _ := run.Config["validationConfigHash"].(string)
|
||||
if stagedConfigHash == "" {
|
||||
return GatewayTrafficMode{}, fmt.Errorf("%w: certified capacity profiles were not staged", ErrAcceptancePromotionGates)
|
||||
}
|
||||
if stagedConfigHash != strings.ToLower(strings.TrimSpace(input.ConfigHash)) {
|
||||
return GatewayTrafficMode{}, fmt.Errorf("%w: staged capacity config hash changed", ErrAcceptancePromotionGates)
|
||||
}
|
||||
stagedProfilesHash, _ := run.Config["validationProfilesHash"].(string)
|
||||
if stagedProfilesHash == "" || stagedProfilesHash != capacityProfileInputsHash(input.CapacityProfiles) {
|
||||
return GatewayTrafficMode{}, fmt.Errorf("%w: staged capacity profiles changed", ErrAcceptancePromotionGates)
|
||||
}
|
||||
if err := replaceCapacityProfilesTx(ctx, tx, run, input.ConfigHash, input.CapacityProfiles); err != nil {
|
||||
return GatewayTrafficMode{}, fmt.Errorf("%w: %v", ErrAcceptancePromotionGates, err)
|
||||
}
|
||||
next := GatewayTrafficMode{
|
||||
Mode: "live", RunID: run.ID, Revision: current.Revision + 1,
|
||||
ReleaseSHA: run.ReleaseSHA, APIImageDigest: run.APIImageDigest,
|
||||
WorkerImageDigest: run.WorkerImageDigest,
|
||||
ConfigHash: strings.ToLower(strings.TrimSpace(input.ConfigHash)),
|
||||
}
|
||||
nextValue, _ := json.Marshal(next)
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE system_settings
|
||||
@@ -350,6 +397,69 @@ WHERE id = $1::uuid`, current.RunID); err != nil {
|
||||
return next, nil
|
||||
}
|
||||
|
||||
func (s *Store) PauseGatewayTraffic(
|
||||
ctx context.Context,
|
||||
input PauseGatewayTrafficInput,
|
||||
) (GatewayTrafficMode, error) {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return GatewayTrafficMode{}, err
|
||||
}
|
||||
defer rollbackTransaction(tx)
|
||||
var value []byte
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT value
|
||||
FROM system_settings
|
||||
WHERE setting_key = $1
|
||||
FOR UPDATE`, SystemSettingGatewayTrafficMode).Scan(&value); err != nil {
|
||||
return GatewayTrafficMode{}, err
|
||||
}
|
||||
var current GatewayTrafficMode
|
||||
if err := json.Unmarshal(value, ¤t); err != nil {
|
||||
return GatewayTrafficMode{}, err
|
||||
}
|
||||
reason := strings.TrimSpace(input.Reason)
|
||||
if normalizeTrafficMode(current.Mode) != "live" ||
|
||||
current.Revision != input.Revision ||
|
||||
current.ReleaseSHA != strings.TrimSpace(input.ReleaseSHA) ||
|
||||
current.APIImageDigest != strings.TrimSpace(input.APIImageDigest) ||
|
||||
current.WorkerImageDigest != strings.TrimSpace(input.WorkerImageDigest) ||
|
||||
reason == "" || len(reason) > 240 {
|
||||
return GatewayTrafficMode{}, ErrAcceptanceStateConflict
|
||||
}
|
||||
next := GatewayTrafficMode{
|
||||
Mode: "validation", RunID: current.RunID, Revision: current.Revision + 1,
|
||||
ReleaseSHA: current.ReleaseSHA, APIImageDigest: current.APIImageDigest,
|
||||
WorkerImageDigest: current.WorkerImageDigest, ConfigHash: current.ConfigHash,
|
||||
}
|
||||
nextValue, _ := json.Marshal(next)
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE system_settings
|
||||
SET value = $2::jsonb, updated_at = now()
|
||||
WHERE setting_key = $1`, SystemSettingGatewayTrafficMode, nextValue); err != nil {
|
||||
return GatewayTrafficMode{}, err
|
||||
}
|
||||
if current.RunID != "" {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE gateway_acceptance_runs
|
||||
SET report = jsonb_set(
|
||||
report,
|
||||
'{postPromotionPause}',
|
||||
jsonb_build_object('reason',$2::text,'pausedAt',now()),
|
||||
true
|
||||
),
|
||||
updated_at = now()
|
||||
WHERE id = $1::uuid`, current.RunID, reason); err != nil {
|
||||
return GatewayTrafficMode{}, err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return GatewayTrafficMode{}, err
|
||||
}
|
||||
next.UpdatedAt = time.Now()
|
||||
return next, nil
|
||||
}
|
||||
|
||||
func (s *Store) AbortAcceptanceRun(ctx context.Context, input PromoteAcceptanceRunInput) (GatewayTrafficMode, error) {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user