引入动态流量门禁、隔离验收身份与协议级 Gemini/Volces 模拟器,覆盖双站点 API、Worker、PostgreSQL、River、账务、回调和媒体物化链路。 新增 P24/P28/P32 容量阶梯、Worker 强杀恢复、真实小流量 canary、CAS 放量和失败保持 validation 的生产编排;Worker 执行槽、连接池、媒体并发和双站点副本数改为环境配置。 验证:Go 全量测试、真实 PostgreSQL 迁移集成测试、迁移安全检查、OpenAPI 生成、ShellCheck、Kustomize、gofmt 和 git diff --check。
534 lines
19 KiB
Go
534 lines
19 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
const SystemSettingGatewayTrafficMode = "gateway_traffic_mode"
|
|
|
|
var (
|
|
acceptanceReleaseSHAPattern = regexp.MustCompile(`^[0-9a-f]{40}$`)
|
|
acceptanceDigestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
|
)
|
|
|
|
var (
|
|
ErrProductionTrafficPaused = errors.New("production traffic is paused for validation")
|
|
ErrAcceptanceNotAuthorized = errors.New("acceptance request is not authorized")
|
|
ErrAcceptanceRunNotActive = errors.New("acceptance run is not active")
|
|
ErrAcceptanceStateConflict = errors.New("acceptance state changed")
|
|
ErrAcceptancePromotionGates = errors.New("acceptance promotion gates have not passed")
|
|
)
|
|
|
|
type GatewayTrafficMode struct {
|
|
Mode string `json:"mode"`
|
|
RunID string `json:"runId,omitempty"`
|
|
Revision int64 `json:"revision"`
|
|
ReleaseSHA string `json:"releaseSha,omitempty"`
|
|
APIImageDigest string `json:"apiImageDigest,omitempty"`
|
|
WorkerImageDigest string `json:"workerImageDigest,omitempty"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
type AcceptanceRun struct {
|
|
ID string `json:"id"`
|
|
Status string `json:"status"`
|
|
ReleaseSHA string `json:"releaseSha"`
|
|
APIImageDigest string `json:"apiImageDigest"`
|
|
WorkerImageDigest string `json:"workerImageDigest"`
|
|
APIKeyID string `json:"apiKeyId"`
|
|
UserID string `json:"userId"`
|
|
EmulatorBaseURL string `json:"emulatorBaseUrl"`
|
|
CallbackURL string `json:"callbackUrl,omitempty"`
|
|
CapacityProfile string `json:"capacityProfile"`
|
|
Config map[string]any `json:"config"`
|
|
Report map[string]any `json:"report"`
|
|
FailureReason string `json:"failureReason,omitempty"`
|
|
StartedAt string `json:"startedAt,omitempty"`
|
|
FinishedAt string `json:"finishedAt,omitempty"`
|
|
PromotedAt string `json:"promotedAt,omitempty"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
type CreateAcceptanceRunInput struct {
|
|
ReleaseSHA string `json:"releaseSha"`
|
|
APIImageDigest string `json:"apiImageDigest"`
|
|
WorkerImageDigest string `json:"workerImageDigest"`
|
|
APIKeyID string `json:"apiKeyId"`
|
|
UserID string `json:"userId"`
|
|
Token string `json:"token"`
|
|
EmulatorBaseURL string `json:"emulatorBaseUrl"`
|
|
CallbackURL string `json:"callbackUrl,omitempty"`
|
|
CapacityProfile string `json:"capacityProfile"`
|
|
Config map[string]any `json:"config,omitempty"`
|
|
}
|
|
|
|
type FinishAcceptanceRunInput struct {
|
|
RunID string `json:"-"`
|
|
Passed bool `json:"passed"`
|
|
Report map[string]any `json:"report"`
|
|
FailureReason string `json:"failureReason,omitempty"`
|
|
}
|
|
|
|
type PromoteAcceptanceRunInput struct {
|
|
RunID string `json:"-"`
|
|
Revision int64 `json:"revision"`
|
|
ReleaseSHA string `json:"releaseSha"`
|
|
APIImageDigest string `json:"apiImageDigest"`
|
|
WorkerImageDigest string `json:"workerImageDigest"`
|
|
}
|
|
|
|
func DefaultGatewayTrafficMode() GatewayTrafficMode {
|
|
return GatewayTrafficMode{Mode: "live"}
|
|
}
|
|
|
|
func (s *Store) GetGatewayTrafficMode(ctx context.Context) (GatewayTrafficMode, error) {
|
|
var value []byte
|
|
var updatedAt time.Time
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT value, updated_at
|
|
FROM system_settings
|
|
WHERE setting_key = $1`, SystemSettingGatewayTrafficMode).Scan(&value, &updatedAt)
|
|
if err != nil {
|
|
if IsNotFound(err) || IsUndefinedDatabaseObject(err) {
|
|
return DefaultGatewayTrafficMode(), nil
|
|
}
|
|
return GatewayTrafficMode{}, err
|
|
}
|
|
var mode GatewayTrafficMode
|
|
if err := json.Unmarshal(value, &mode); err != nil {
|
|
return GatewayTrafficMode{}, fmt.Errorf("decode gateway traffic mode: %w", err)
|
|
}
|
|
mode.Mode = normalizeTrafficMode(mode.Mode)
|
|
mode.UpdatedAt = updatedAt
|
|
return mode, nil
|
|
}
|
|
|
|
func (s *Store) CreateAcceptanceRun(ctx context.Context, input CreateAcceptanceRunInput) (AcceptanceRun, error) {
|
|
input.ReleaseSHA = strings.TrimSpace(input.ReleaseSHA)
|
|
input.APIImageDigest = strings.TrimSpace(input.APIImageDigest)
|
|
input.WorkerImageDigest = strings.TrimSpace(input.WorkerImageDigest)
|
|
input.APIKeyID = strings.TrimSpace(input.APIKeyID)
|
|
input.UserID = strings.TrimSpace(input.UserID)
|
|
input.Token = strings.TrimSpace(input.Token)
|
|
input.EmulatorBaseURL = strings.TrimRight(strings.TrimSpace(input.EmulatorBaseURL), "/")
|
|
input.CallbackURL = strings.TrimSpace(input.CallbackURL)
|
|
input.CapacityProfile = strings.ToUpper(strings.TrimSpace(input.CapacityProfile))
|
|
if input.CapacityProfile == "" {
|
|
input.CapacityProfile = "P24"
|
|
}
|
|
if input.ReleaseSHA == "" || input.APIImageDigest == "" || input.WorkerImageDigest == "" ||
|
|
input.APIKeyID == "" || input.UserID == "" || input.Token == "" || input.EmulatorBaseURL == "" ||
|
|
input.CallbackURL == "" {
|
|
return AcceptanceRun{}, errors.New("release SHA, image digests, API key, user, token, emulator URL, and callback URL are required")
|
|
}
|
|
if !acceptanceReleaseSHAPattern.MatchString(input.ReleaseSHA) ||
|
|
!acceptanceDigestPattern.MatchString(input.APIImageDigest) ||
|
|
!acceptanceDigestPattern.MatchString(input.WorkerImageDigest) {
|
|
return AcceptanceRun{}, errors.New("release SHA and image digests must use full immutable identifiers")
|
|
}
|
|
if len(input.Token) < 32 {
|
|
return AcceptanceRun{}, errors.New("acceptance token must contain at least 32 characters")
|
|
}
|
|
if !validAcceptanceURL(input.EmulatorBaseURL) || !validAcceptanceURL(input.CallbackURL) {
|
|
return AcceptanceRun{}, errors.New("emulator and callback URLs must be absolute HTTP URLs without user information")
|
|
}
|
|
switch input.CapacityProfile {
|
|
case "P24", "P28", "P32":
|
|
default:
|
|
return AcceptanceRun{}, errors.New("capacity profile must be P24, P28, or P32")
|
|
}
|
|
config, _ := json.Marshal(sanitizeAcceptanceMetadata(sanitizeJSONForStorage(input.Config)))
|
|
tokenHash := acceptanceTokenSHA256(input.Token)
|
|
return scanAcceptanceRun(s.pool.QueryRow(ctx, `
|
|
INSERT INTO gateway_acceptance_runs (
|
|
release_sha, api_image_digest, worker_image_digest, api_key_id, user_id,
|
|
token_sha256, emulator_base_url, callback_url, capacity_profile, config
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, NULLIF($8, ''), $9, $10::jsonb)
|
|
RETURNING `+acceptanceRunColumns,
|
|
input.ReleaseSHA, input.APIImageDigest, input.WorkerImageDigest, input.APIKeyID, input.UserID,
|
|
tokenHash, input.EmulatorBaseURL, input.CallbackURL, input.CapacityProfile, config,
|
|
))
|
|
}
|
|
|
|
func (s *Store) GetAcceptanceRun(ctx context.Context, runID string) (AcceptanceRun, error) {
|
|
return scanAcceptanceRun(s.pool.QueryRow(ctx, `
|
|
SELECT `+acceptanceRunColumns+`
|
|
FROM gateway_acceptance_runs
|
|
WHERE id = $1::uuid`, strings.TrimSpace(runID)))
|
|
}
|
|
|
|
func (s *Store) ActivateAcceptanceRun(ctx context.Context, runID string) (GatewayTrafficMode, error) {
|
|
tx, err := s.pool.Begin(ctx)
|
|
if err != nil {
|
|
return GatewayTrafficMode{}, err
|
|
}
|
|
defer rollbackTransaction(tx)
|
|
|
|
var currentValue []byte
|
|
if err := tx.QueryRow(ctx, `
|
|
SELECT value
|
|
FROM system_settings
|
|
WHERE setting_key = $1
|
|
FOR UPDATE`, SystemSettingGatewayTrafficMode).Scan(¤tValue); err != nil {
|
|
return GatewayTrafficMode{}, err
|
|
}
|
|
var current GatewayTrafficMode
|
|
if err := json.Unmarshal(currentValue, ¤t); err != nil {
|
|
return GatewayTrafficMode{}, err
|
|
}
|
|
if normalizeTrafficMode(current.Mode) != "live" {
|
|
return GatewayTrafficMode{}, ErrAcceptanceStateConflict
|
|
}
|
|
run, err := scanAcceptanceRun(tx.QueryRow(ctx, `
|
|
SELECT `+acceptanceRunColumns+`
|
|
FROM gateway_acceptance_runs
|
|
WHERE id = $1::uuid
|
|
FOR UPDATE`, strings.TrimSpace(runID)))
|
|
if err != nil {
|
|
return GatewayTrafficMode{}, err
|
|
}
|
|
if run.Status != "pending" && run.Status != "failed" {
|
|
return GatewayTrafficMode{}, ErrAcceptanceStateConflict
|
|
}
|
|
next := GatewayTrafficMode{
|
|
Mode: "validation",
|
|
RunID: run.ID,
|
|
Revision: current.Revision + 1,
|
|
ReleaseSHA: run.ReleaseSHA,
|
|
APIImageDigest: run.APIImageDigest,
|
|
WorkerImageDigest: run.WorkerImageDigest,
|
|
}
|
|
value, _ := json.Marshal(next)
|
|
if _, err := tx.Exec(ctx, `
|
|
UPDATE system_settings
|
|
SET value = $2::jsonb, updated_at = now()
|
|
WHERE setting_key = $1`, SystemSettingGatewayTrafficMode, value); err != nil {
|
|
return GatewayTrafficMode{}, err
|
|
}
|
|
if _, err := tx.Exec(ctx, `
|
|
UPDATE gateway_acceptance_runs
|
|
SET status = 'running', started_at = COALESCE(started_at, now()),
|
|
finished_at = NULL, failure_reason = NULL, updated_at = now()
|
|
WHERE id = $1::uuid`, run.ID); 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) FinishAcceptanceRun(ctx context.Context, input FinishAcceptanceRunInput) (AcceptanceRun, error) {
|
|
status := "failed"
|
|
if input.Passed {
|
|
status = "passed"
|
|
}
|
|
report, _ := json.Marshal(sanitizeAcceptanceMetadata(sanitizeJSONForStorage(input.Report)))
|
|
return scanAcceptanceRun(s.pool.QueryRow(ctx, `
|
|
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'
|
|
RETURNING `+acceptanceRunColumns,
|
|
strings.TrimSpace(input.RunID), status, report, strings.TrimSpace(input.FailureReason),
|
|
))
|
|
}
|
|
|
|
func (s *Store) RetryAcceptanceRun(ctx context.Context, runID string) (AcceptanceRun, error) {
|
|
tx, err := s.pool.Begin(ctx)
|
|
if err != nil {
|
|
return AcceptanceRun{}, 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 AcceptanceRun{}, err
|
|
}
|
|
var mode GatewayTrafficMode
|
|
if err := json.Unmarshal(value, &mode); err != nil {
|
|
return AcceptanceRun{}, err
|
|
}
|
|
if normalizeTrafficMode(mode.Mode) != "validation" || mode.RunID != strings.TrimSpace(runID) {
|
|
return AcceptanceRun{}, ErrAcceptanceStateConflict
|
|
}
|
|
run, err := scanAcceptanceRun(tx.QueryRow(ctx, `
|
|
UPDATE gateway_acceptance_runs
|
|
SET status = 'running', report = '{}'::jsonb, failure_reason = NULL,
|
|
finished_at = NULL, updated_at = now()
|
|
WHERE id = $1::uuid AND status = 'failed'
|
|
RETURNING `+acceptanceRunColumns, strings.TrimSpace(runID)))
|
|
if err != nil {
|
|
return AcceptanceRun{}, err
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return AcceptanceRun{}, err
|
|
}
|
|
return run, nil
|
|
}
|
|
|
|
func (s *Store) PromoteAcceptanceRun(ctx context.Context, input PromoteAcceptanceRunInput) (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
|
|
}
|
|
if normalizeTrafficMode(current.Mode) != "validation" ||
|
|
current.RunID != strings.TrimSpace(input.RunID) ||
|
|
current.Revision != input.Revision ||
|
|
current.ReleaseSHA != strings.TrimSpace(input.ReleaseSHA) ||
|
|
current.APIImageDigest != strings.TrimSpace(input.APIImageDigest) ||
|
|
current.WorkerImageDigest != strings.TrimSpace(input.WorkerImageDigest) {
|
|
return GatewayTrafficMode{}, ErrAcceptanceStateConflict
|
|
}
|
|
var status string
|
|
if err := tx.QueryRow(ctx, `
|
|
SELECT status
|
|
FROM gateway_acceptance_runs
|
|
WHERE id = $1::uuid
|
|
FOR UPDATE`, current.RunID).Scan(&status); err != nil {
|
|
return GatewayTrafficMode{}, err
|
|
}
|
|
if status != "passed" {
|
|
return GatewayTrafficMode{}, ErrAcceptancePromotionGates
|
|
}
|
|
next := GatewayTrafficMode{Mode: "live", Revision: current.Revision + 1}
|
|
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 _, err := tx.Exec(ctx, `
|
|
UPDATE gateway_acceptance_runs
|
|
SET status = 'promoted', promoted_at = now(), updated_at = now()
|
|
WHERE id = $1::uuid`, current.RunID); 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 {
|
|
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
|
|
}
|
|
if normalizeTrafficMode(current.Mode) != "validation" ||
|
|
current.RunID != strings.TrimSpace(input.RunID) ||
|
|
current.Revision != input.Revision ||
|
|
current.ReleaseSHA != strings.TrimSpace(input.ReleaseSHA) ||
|
|
current.APIImageDigest != strings.TrimSpace(input.APIImageDigest) ||
|
|
current.WorkerImageDigest != strings.TrimSpace(input.WorkerImageDigest) {
|
|
return GatewayTrafficMode{}, ErrAcceptanceStateConflict
|
|
}
|
|
next := GatewayTrafficMode{Mode: "live", Revision: current.Revision + 1}
|
|
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 _, err := tx.Exec(ctx, `
|
|
UPDATE gateway_acceptance_runs
|
|
SET status = 'aborted', finished_at = COALESCE(finished_at, now()), updated_at = now()
|
|
WHERE id = $1::uuid AND status IN ('running', 'failed', 'passed')`, current.RunID); 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) AuthorizeAcceptanceTask(ctx context.Context, runID string, token string, user *auth.User) (string, error) {
|
|
mode, err := s.GetGatewayTrafficMode(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
runID = strings.TrimSpace(runID)
|
|
token = strings.TrimSpace(token)
|
|
if mode.Mode == "live" {
|
|
if runID != "" || token != "" {
|
|
return "", ErrAcceptanceRunNotActive
|
|
}
|
|
return "", nil
|
|
}
|
|
if runID == "" || token == "" || runID != mode.RunID {
|
|
return "", ErrProductionTrafficPaused
|
|
}
|
|
if user == nil || strings.TrimSpace(user.APIKeyID) == "" {
|
|
return "", ErrAcceptanceNotAuthorized
|
|
}
|
|
var activeRunID, apiKeyID, userID, tokenHash, status string
|
|
err = s.pool.QueryRow(ctx, `
|
|
SELECT id::text, api_key_id, user_id, token_sha256, status
|
|
FROM gateway_acceptance_runs
|
|
WHERE id = $1::uuid`, runID).Scan(&activeRunID, &apiKeyID, &userID, &tokenHash, &status)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return "", ErrAcceptanceNotAuthorized
|
|
}
|
|
return "", err
|
|
}
|
|
if status != "running" || apiKeyID != strings.TrimSpace(user.APIKeyID) || userID != strings.TrimSpace(user.ID) ||
|
|
subtle.ConstantTimeCompare([]byte(tokenHash), []byte(acceptanceTokenSHA256(token))) != 1 {
|
|
return "", ErrAcceptanceNotAuthorized
|
|
}
|
|
return activeRunID, nil
|
|
}
|
|
|
|
func (s *Store) AcceptanceCandidateOverride(ctx context.Context, runID string) (string, string, error) {
|
|
var baseURL, status string
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT emulator_base_url, status
|
|
FROM gateway_acceptance_runs
|
|
WHERE id = $1::uuid`, strings.TrimSpace(runID)).Scan(&baseURL, &status)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if status != "running" && status != "passed" {
|
|
return "", "", ErrAcceptanceRunNotActive
|
|
}
|
|
return strings.TrimRight(strings.TrimSpace(baseURL), "/"), "acceptance-protocol-emulator", nil
|
|
}
|
|
|
|
func (s *Store) AcceptanceCallbackURL(ctx context.Context, taskID string) (string, bool, error) {
|
|
var callbackURL string
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT COALESCE(run.callback_url, '')
|
|
FROM gateway_tasks task
|
|
JOIN gateway_acceptance_runs run ON run.id = task.acceptance_run_id
|
|
WHERE task.id = $1::uuid`, strings.TrimSpace(taskID)).Scan(&callbackURL)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return "", false, nil
|
|
}
|
|
return "", false, err
|
|
}
|
|
return strings.TrimSpace(callbackURL), true, nil
|
|
}
|
|
|
|
func normalizeTrafficMode(value string) string {
|
|
if strings.EqualFold(strings.TrimSpace(value), "validation") {
|
|
return "validation"
|
|
}
|
|
return "live"
|
|
}
|
|
|
|
func acceptanceTokenSHA256(token string) string {
|
|
sum := sha256.Sum256([]byte(strings.TrimSpace(token)))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func validAcceptanceURL(raw string) bool {
|
|
parsed, err := url.Parse(strings.TrimSpace(raw))
|
|
if err != nil || parsed.User != nil || parsed.Host == "" {
|
|
return false
|
|
}
|
|
return parsed.Scheme == "http" || parsed.Scheme == "https"
|
|
}
|
|
|
|
func sanitizeAcceptanceMetadata(value any) any {
|
|
switch typed := value.(type) {
|
|
case map[string]any:
|
|
next := make(map[string]any, len(typed))
|
|
for key, item := range typed {
|
|
normalized := strings.ToLower(strings.NewReplacer("_", "", "-", "", ".", "").Replace(key))
|
|
if strings.Contains(normalized, "password") ||
|
|
strings.Contains(normalized, "secret") ||
|
|
strings.Contains(normalized, "credential") ||
|
|
strings.Contains(normalized, "connectionstring") ||
|
|
strings.Contains(normalized, "databaseurl") ||
|
|
(strings.Contains(normalized, "token") && !strings.HasSuffix(normalized, "count")) {
|
|
next[key] = "[REDACTED]"
|
|
continue
|
|
}
|
|
next[key] = sanitizeAcceptanceMetadata(item)
|
|
}
|
|
return next
|
|
case []any:
|
|
next := make([]any, len(typed))
|
|
for index, item := range typed {
|
|
next[index] = sanitizeAcceptanceMetadata(item)
|
|
}
|
|
return next
|
|
default:
|
|
return value
|
|
}
|
|
}
|
|
|
|
const acceptanceRunColumns = `
|
|
id::text, status, release_sha, api_image_digest, worker_image_digest,
|
|
api_key_id, user_id, emulator_base_url, COALESCE(callback_url, ''),
|
|
capacity_profile, config, report, COALESCE(failure_reason, ''),
|
|
COALESCE(started_at::text, ''), COALESCE(finished_at::text, ''),
|
|
COALESCE(promoted_at::text, ''), created_at, updated_at`
|
|
|
|
func scanAcceptanceRun(scanner taskScanner) (AcceptanceRun, error) {
|
|
var run AcceptanceRun
|
|
var config, report []byte
|
|
err := scanner.Scan(
|
|
&run.ID, &run.Status, &run.ReleaseSHA, &run.APIImageDigest, &run.WorkerImageDigest,
|
|
&run.APIKeyID, &run.UserID, &run.EmulatorBaseURL, &run.CallbackURL,
|
|
&run.CapacityProfile, &config, &report, &run.FailureReason,
|
|
&run.StartedAt, &run.FinishedAt, &run.PromotedAt, &run.CreatedAt, &run.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return AcceptanceRun{}, err
|
|
}
|
|
run.Config = decodeObject(config)
|
|
run.Report = decodeObject(report)
|
|
return run, nil
|
|
}
|