440 lines
22 KiB
Go
440 lines
22 KiB
Go
package diagnostics
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"data-terminal-cloud/internal/audit"
|
|
"data-terminal-cloud/internal/buildinfo"
|
|
"data-terminal-cloud/internal/idempotency"
|
|
"data-terminal-cloud/internal/identity"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
var (
|
|
acceptanceCandidatePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$`)
|
|
acceptanceDigestPattern = regexp.MustCompile(`^sha256:[a-f0-9]{64}$`)
|
|
acceptanceEnvironments = map[string]bool{"LAB": true, "STAGING": true, "PRODUCTION": true}
|
|
acceptanceRunTypes = map[string]bool{"REAL_DEVICE_PROVIDER": true, "PILOT": true, "PRODUCTION_RELEASE": true}
|
|
acceptanceDependencies = map[string]string{"oidc_jwks": "api", "ark_tls": "worker", "volc_rtc_tls": "worker", "content_safety_tls": "worker"}
|
|
)
|
|
|
|
type AcceptanceSnapshotInput struct {
|
|
CandidateVersion string `json:"candidate_version"`
|
|
BuildDigest string `json:"build_digest"`
|
|
Environment string `json:"environment"`
|
|
RunType string `json:"run_type"`
|
|
Scope string `json:"scope"`
|
|
RequiredDependencies []string `json:"required_dependencies"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
type AcceptanceDatabaseState struct {
|
|
MigrationVersion int64 `json:"migration_version"`
|
|
Dirty bool `json:"dirty"`
|
|
}
|
|
|
|
type AcceptanceDependencyState struct {
|
|
ServiceName string `json:"service_name"`
|
|
Dependency string `json:"dependency"`
|
|
Status string `json:"status"`
|
|
CheckedAt *time.Time `json:"checked_at,omitempty"`
|
|
LastSuccessAt *time.Time `json:"last_success_at,omitempty"`
|
|
LastFailureAt *time.Time `json:"last_failure_at,omitempty"`
|
|
LastErrorCode string `json:"last_error_code,omitempty"`
|
|
LatencyMS int64 `json:"latency_ms"`
|
|
ProbeIntervalMS int64 `json:"probe_interval_ms"`
|
|
ConsecutiveFailures int `json:"consecutive_failures"`
|
|
Fresh bool `json:"fresh"`
|
|
GateSatisfied bool `json:"gate_satisfied"`
|
|
}
|
|
|
|
type AcceptanceOutboxState struct {
|
|
Pending int64 `json:"pending"`
|
|
Processing int64 `json:"processing"`
|
|
Dead int64 `json:"dead"`
|
|
}
|
|
|
|
type AcceptanceAlertState struct {
|
|
CriticalOpen int64 `json:"critical_open"`
|
|
HighOpen int64 `json:"high_open"`
|
|
Blocking int64 `json:"blocking"`
|
|
BlockingItems []AcceptanceBlockingAlert `json:"blocking_items"`
|
|
BlockingItemsTruncated bool `json:"blocking_items_truncated"`
|
|
}
|
|
|
|
type AcceptancePlatformHealthState struct {
|
|
State string `json:"state"`
|
|
Status string `json:"status,omitempty"`
|
|
ObservedAt *time.Time `json:"observed_at,omitempty"`
|
|
ReceivedAt *time.Time `json:"received_at,omitempty"`
|
|
CheckCount int `json:"check_count"`
|
|
WarningCount int `json:"warning_count"`
|
|
CriticalCount int `json:"critical_count"`
|
|
GateSatisfied bool `json:"gate_satisfied"`
|
|
}
|
|
|
|
type AcceptanceProductionInfrastructureState struct {
|
|
Applicable bool `json:"applicable"`
|
|
DeploymentProfile string `json:"deployment_profile"`
|
|
OffsiteBackupReady bool `json:"offsite_backup_ready"`
|
|
ExternalAlertReceiverReady bool `json:"external_alert_receiver_ready"`
|
|
MockServicesDisabled bool `json:"mock_services_disabled"`
|
|
GateSatisfied bool `json:"gate_satisfied"`
|
|
}
|
|
|
|
type AcceptanceBlockingAlert struct {
|
|
ID string `json:"id"`
|
|
RuleCode string `json:"rule_code"`
|
|
Severity string `json:"severity"`
|
|
Status string `json:"status"`
|
|
ResourceType string `json:"resource_type,omitempty"`
|
|
ResourceID string `json:"resource_id,omitempty"`
|
|
ResourceState string `json:"resource_state"`
|
|
FirstDetectedAt time.Time `json:"first_detected_at"`
|
|
LastDetectedAt time.Time `json:"last_detected_at"`
|
|
}
|
|
|
|
type AcceptanceGate struct {
|
|
Code string `json:"code"`
|
|
Satisfied bool `json:"satisfied"`
|
|
Summary string `json:"summary"`
|
|
}
|
|
|
|
type AcceptanceSnapshot struct {
|
|
ID string `json:"id"`
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
TenantID string `json:"tenant_id"`
|
|
CandidateVersion string `json:"candidate_version"`
|
|
BuildDigest string `json:"build_digest"`
|
|
Environment string `json:"environment"`
|
|
RunType string `json:"run_type"`
|
|
Scope string `json:"scope"`
|
|
RedactionPolicy string `json:"redaction_policy"`
|
|
RuntimeBuild buildinfo.Info `json:"runtime_build"`
|
|
Database AcceptanceDatabaseState `json:"database"`
|
|
IntegrationIssues IntegrationIssueSummary `json:"integration_issues"`
|
|
ExternalDependencies []AcceptanceDependencyState `json:"external_dependencies"`
|
|
Outbox AcceptanceOutboxState `json:"outbox"`
|
|
Alerts AcceptanceAlertState `json:"alerts"`
|
|
PlatformHealth AcceptancePlatformHealthState `json:"platform_health"`
|
|
ProductionInfrastructure AcceptanceProductionInfrastructureState `json:"production_infrastructure"`
|
|
Gates []AcceptanceGate `json:"gates"`
|
|
OverallGateSatisfied bool `json:"overall_gate_satisfied"`
|
|
Disclaimer string `json:"disclaimer"`
|
|
}
|
|
|
|
func (s *Service) ExportAcceptanceSnapshot(ctx context.Context, principal identity.Principal, tenantPublicID, key, requestID string, input AcceptanceSnapshotInput) (AcceptanceSnapshot, bool, error) {
|
|
normalizeAcceptanceSnapshotInput(&input)
|
|
if !validAcceptanceSnapshotInput(input) {
|
|
return AcceptanceSnapshot{}, false, ErrInvalid
|
|
}
|
|
a, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_operator", "ops_admin")
|
|
if err != nil {
|
|
return AcceptanceSnapshot{}, false, err
|
|
}
|
|
requestBody, _ := json.Marshal(input)
|
|
result, err := s.idem.Execute(ctx, idempotency.Request{
|
|
TenantID: &a.tenantID, ActorKey: "ops:" + a.userPublicID,
|
|
Scope: "POST /api/v1/ops/diagnostics/acceptance-snapshots", Key: key,
|
|
RequestHash: idempotency.Hash(requestBody), ExpiresAfter: 24 * time.Hour,
|
|
}, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) {
|
|
snapshot, snapshotErr := buildAcceptanceSnapshot(operationContext, tx, tenantPublicID, a.tenantID, input)
|
|
if snapshotErr != nil {
|
|
return idempotency.Result{}, snapshotErr
|
|
}
|
|
if auditErr := s.audit.Record(operationContext, tx, audit.Event{
|
|
TenantID: &a.tenantID, ActorType: "user", ActorPublicID: &a.userPublicID,
|
|
Action: "ops.diagnostics.acceptance_snapshot.export", ResourceType: "acceptance_snapshot", ResourceID: &snapshot.ID,
|
|
RequestID: requestID, Purpose: input.Reason, Result: "success",
|
|
Detail: map[string]any{
|
|
"candidate_version": input.CandidateVersion, "build_digest": input.BuildDigest,
|
|
"environment": input.Environment, "run_type": input.RunType, "scope": input.Scope,
|
|
"required_dependencies": input.RequiredDependencies, "overall_gate_satisfied": snapshot.OverallGateSatisfied,
|
|
"redaction_policy": snapshot.RedactionPolicy,
|
|
},
|
|
}); auditErr != nil {
|
|
return idempotency.Result{}, auditErr
|
|
}
|
|
encoded, _ := json.Marshal(snapshot)
|
|
return idempotency.Result{StatusCode: 201, Body: encoded, ResourceID: &snapshot.ID}, nil
|
|
})
|
|
if err != nil {
|
|
return AcceptanceSnapshot{}, false, err
|
|
}
|
|
var snapshot AcceptanceSnapshot
|
|
if err = json.Unmarshal(result.Body, &snapshot); err != nil {
|
|
return AcceptanceSnapshot{}, false, err
|
|
}
|
|
return snapshot, !result.Replayed, nil
|
|
}
|
|
|
|
func normalizeAcceptanceSnapshotInput(input *AcceptanceSnapshotInput) {
|
|
input.CandidateVersion = strings.TrimSpace(input.CandidateVersion)
|
|
input.BuildDigest = strings.ToLower(strings.TrimSpace(input.BuildDigest))
|
|
input.Environment = strings.ToUpper(strings.TrimSpace(input.Environment))
|
|
input.RunType = strings.ToUpper(strings.TrimSpace(input.RunType))
|
|
input.Scope = strings.TrimSpace(input.Scope)
|
|
input.Reason = strings.TrimSpace(input.Reason)
|
|
for index := range input.RequiredDependencies {
|
|
input.RequiredDependencies[index] = strings.ToLower(strings.TrimSpace(input.RequiredDependencies[index]))
|
|
}
|
|
sort.Strings(input.RequiredDependencies)
|
|
}
|
|
|
|
func validAcceptanceSnapshotInput(input AcceptanceSnapshotInput) bool {
|
|
if !acceptanceCandidatePattern.MatchString(input.CandidateVersion) || !acceptanceDigestPattern.MatchString(input.BuildDigest) ||
|
|
!acceptanceEnvironments[input.Environment] || !acceptanceRunTypes[input.RunType] ||
|
|
input.Scope == "" || len([]rune(input.Scope)) > 500 || input.Reason == "" || len([]rune(input.Reason)) > 255 ||
|
|
len(input.RequiredDependencies) == 0 || len(input.RequiredDependencies) > len(acceptanceDependencies) {
|
|
return false
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, dependency := range input.RequiredDependencies {
|
|
if _, allowed := acceptanceDependencies[dependency]; !allowed || seen[dependency] {
|
|
return false
|
|
}
|
|
seen[dependency] = true
|
|
}
|
|
return true
|
|
}
|
|
|
|
func buildAcceptanceSnapshot(ctx context.Context, tx pgx.Tx, tenantPublicID, tenantInternalID string, input AcceptanceSnapshotInput) (AcceptanceSnapshot, error) {
|
|
now := time.Now().UTC()
|
|
snapshot := AcceptanceSnapshot{
|
|
ID: uuid.NewString(), GeneratedAt: now, TenantID: tenantPublicID,
|
|
CandidateVersion: input.CandidateVersion, BuildDigest: input.BuildDigest, Environment: input.Environment,
|
|
RunType: input.RunType, Scope: input.Scope, RedactionPolicy: "ops-acceptance-minimal-v1",
|
|
RuntimeBuild: buildinfo.Current(), Alerts: AcceptanceAlertState{BlockingItems: []AcceptanceBlockingAlert{}},
|
|
Disclaimer: "本快照仅证明生成时刻的技术预检状态,不替代真实设备、真实供应商、灰度观察或发布审批结论。",
|
|
}
|
|
if err := tx.QueryRow(ctx, `SELECT version,dirty FROM schema_migrations ORDER BY version DESC LIMIT 1`).Scan(&snapshot.Database.MigrationVersion, &snapshot.Database.Dirty); err != nil {
|
|
return AcceptanceSnapshot{}, err
|
|
}
|
|
if err := tx.QueryRow(ctx, `
|
|
SELECT count(*),
|
|
count(*) FILTER (WHERE status <> 'CLOSED'),
|
|
count(*) FILTER (WHERE severity='CRITICAL' AND status <> 'CLOSED'),
|
|
count(*) FILTER (WHERE severity='HIGH' AND status <> 'CLOSED'),
|
|
count(*) FILTER (WHERE severity IN ('CRITICAL','HIGH') AND status <> 'CLOSED')
|
|
FROM integration_issue WHERE tenant_id=$1::uuid`, tenantInternalID).Scan(
|
|
&snapshot.IntegrationIssues.Total, &snapshot.IntegrationIssues.Open, &snapshot.IntegrationIssues.CriticalOpen,
|
|
&snapshot.IntegrationIssues.HighOpen, &snapshot.IntegrationIssues.ReleaseBlocking,
|
|
); err != nil {
|
|
return AcceptanceSnapshot{}, err
|
|
}
|
|
snapshot.IntegrationIssues.ReleaseGateSatisfied = snapshot.IntegrationIssues.ReleaseBlocking == 0
|
|
|
|
for _, dependency := range input.RequiredDependencies {
|
|
state := AcceptanceDependencyState{ServiceName: acceptanceDependencies[dependency], Dependency: dependency, Status: "MISSING"}
|
|
var checkedAt time.Time
|
|
err := tx.QueryRow(ctx, `SELECT status,checked_at,last_success_at,last_failure_at,COALESCE(last_error_code,''),latency_ms,probe_interval_ms,consecutive_failures FROM external_dependency_probe_state WHERE service_name=$1 AND dependency=$2`, state.ServiceName, dependency).Scan(
|
|
&state.Status, &checkedAt, &state.LastSuccessAt, &state.LastFailureAt, &state.LastErrorCode,
|
|
&state.LatencyMS, &state.ProbeIntervalMS, &state.ConsecutiveFailures,
|
|
)
|
|
if err != nil && err != pgx.ErrNoRows {
|
|
return AcceptanceSnapshot{}, err
|
|
}
|
|
if err == nil {
|
|
state.CheckedAt = &checkedAt
|
|
state.Fresh = !checkedAt.After(now) && now.Sub(checkedAt) <= 3*time.Duration(state.ProbeIntervalMS)*time.Millisecond
|
|
state.GateSatisfied = state.Status == "AVAILABLE" && state.Fresh
|
|
}
|
|
snapshot.ExternalDependencies = append(snapshot.ExternalDependencies, state)
|
|
}
|
|
if err := tx.QueryRow(ctx, `SELECT
|
|
count(*) FILTER (WHERE status='pending'),
|
|
count(*) FILTER (WHERE status='processing'),
|
|
count(*) FILTER (WHERE status='dead')
|
|
FROM outbox_event WHERE tenant_id=$1::uuid OR tenant_id IS NULL`, tenantInternalID).Scan(
|
|
&snapshot.Outbox.Pending, &snapshot.Outbox.Processing, &snapshot.Outbox.Dead,
|
|
); err != nil {
|
|
return AcceptanceSnapshot{}, err
|
|
}
|
|
rows, err := tx.Query(ctx, `SELECT public_id::text,rule_code,severity,status,COALESCE(resource_type,''),COALESCE(resource_public_id::text,''),
|
|
CASE
|
|
WHEN resource_public_id IS NULL THEN 'AGGREGATE'
|
|
WHEN resource_type='conversation' THEN CASE WHEN EXISTS(SELECT 1 FROM conversation c WHERE c.public_id=alert_incident.resource_public_id) THEN 'PRESENT' ELSE 'MISSING' END
|
|
WHEN resource_type='privacy_request' THEN CASE WHEN EXISTS(SELECT 1 FROM privacy_request pr WHERE pr.public_id=alert_incident.resource_public_id) THEN 'PRESENT' ELSE 'MISSING' END
|
|
WHEN resource_type='upload_batch' THEN CASE WHEN EXISTS(SELECT 1 FROM upload_batch ub WHERE ub.public_id=alert_incident.resource_public_id) THEN 'PRESENT' ELSE 'MISSING' END
|
|
WHEN resource_type='firmware_rollout' THEN CASE WHEN EXISTS(SELECT 1 FROM firmware_rollout fr WHERE fr.public_id=alert_incident.resource_public_id) THEN 'PRESENT' ELSE 'MISSING' END
|
|
WHEN resource_type='ops_resource_reference' THEN CASE WHEN EXISTS(SELECT 1 FROM ops_resource_reference rr WHERE rr.public_id=alert_incident.resource_public_id) THEN 'PRESENT' ELSE 'MISSING' END
|
|
ELSE 'UNKNOWN'
|
|
END,first_detected_at,last_detected_at
|
|
FROM alert_incident
|
|
WHERE tenant_id=$1::uuid AND status<>'RESOLVED' AND severity IN ('CRITICAL','HIGH')
|
|
ORDER BY CASE severity WHEN 'CRITICAL' THEN 0 ELSE 1 END,first_detected_at,public_id
|
|
LIMIT 100`, tenantInternalID)
|
|
if err != nil {
|
|
return AcceptanceSnapshot{}, err
|
|
}
|
|
for rows.Next() {
|
|
var item AcceptanceBlockingAlert
|
|
if err = rows.Scan(&item.ID, &item.RuleCode, &item.Severity, &item.Status, &item.ResourceType, &item.ResourceID, &item.ResourceState, &item.FirstDetectedAt, &item.LastDetectedAt); err != nil {
|
|
rows.Close()
|
|
return AcceptanceSnapshot{}, err
|
|
}
|
|
snapshot.Alerts.BlockingItems = append(snapshot.Alerts.BlockingItems, item)
|
|
}
|
|
if err = rows.Err(); err != nil {
|
|
rows.Close()
|
|
return AcceptanceSnapshot{}, err
|
|
}
|
|
rows.Close()
|
|
snapshot.Alerts.BlockingItemsTruncated = snapshot.Alerts.Blocking > int64(len(snapshot.Alerts.BlockingItems))
|
|
if err := tx.QueryRow(ctx, `SELECT
|
|
count(*) FILTER (WHERE severity='CRITICAL'),
|
|
count(*) FILTER (WHERE severity='HIGH'),
|
|
count(*)
|
|
FROM alert_incident WHERE tenant_id=$1::uuid AND status<>'RESOLVED' AND severity IN ('CRITICAL','HIGH')`, tenantInternalID).Scan(
|
|
&snapshot.Alerts.CriticalOpen, &snapshot.Alerts.HighOpen, &snapshot.Alerts.Blocking,
|
|
); err != nil {
|
|
return AcceptanceSnapshot{}, err
|
|
}
|
|
platformHealth, productionInfrastructure, err := loadAcceptancePlatformHealth(ctx, tx, now, input.Environment)
|
|
if err != nil {
|
|
return AcceptanceSnapshot{}, err
|
|
}
|
|
snapshot.PlatformHealth = platformHealth
|
|
snapshot.ProductionInfrastructure = productionInfrastructure
|
|
|
|
runtimeSatisfied := input.Environment == "LAB" || (snapshot.RuntimeBuild.Version == input.CandidateVersion && snapshot.RuntimeBuild.Commit != "unknown" && snapshot.RuntimeBuild.BuildTime != "unknown")
|
|
dependenciesSatisfied := true
|
|
for _, dependency := range snapshot.ExternalDependencies {
|
|
dependenciesSatisfied = dependenciesSatisfied && dependency.GateSatisfied
|
|
}
|
|
outboxSatisfied := snapshot.Outbox.Pending == 0 && snapshot.Outbox.Processing == 0 && snapshot.Outbox.Dead == 0
|
|
alertsSatisfied := snapshot.Alerts.Blocking == 0
|
|
snapshot.Gates = []AcceptanceGate{
|
|
{Code: "candidate_build_identified", Satisfied: runtimeSatisfied, Summary: acceptanceRuntimeSummary(input.Environment)},
|
|
{Code: "database_migration_clean", Satisfied: !snapshot.Database.Dirty, Summary: "数据库迁移必须处于非脏状态"},
|
|
{Code: "required_dependencies_available", Satisfied: dependenciesSatisfied, Summary: "所有必需外部依赖探针必须可用且未过期"},
|
|
{Code: "integration_issues_clear", Satisfied: snapshot.IntegrationIssues.ReleaseGateSatisfied, Summary: "不得存在未关闭的高或严重级联调问题"},
|
|
{Code: "outbox_clear", Satisfied: outboxSatisfied, Summary: "租户及平台级待处理、处理中和死信任务必须清零"},
|
|
{Code: "critical_alerts_clear", Satisfied: alertsSatisfied, Summary: "不得存在未解决的高或严重级告警"},
|
|
{Code: "platform_health_current", Satisfied: snapshot.PlatformHealth.GateSatisfied, Summary: acceptancePlatformHealthSummary(snapshot.PlatformHealth)},
|
|
{Code: "production_infrastructure_ready", Satisfied: snapshot.ProductionInfrastructure.GateSatisfied, Summary: acceptanceProductionInfrastructureSummary(snapshot.ProductionInfrastructure)},
|
|
}
|
|
snapshot.OverallGateSatisfied = true
|
|
for _, gate := range snapshot.Gates {
|
|
snapshot.OverallGateSatisfied = snapshot.OverallGateSatisfied && gate.Satisfied
|
|
}
|
|
return snapshot, nil
|
|
}
|
|
|
|
func loadAcceptancePlatformHealth(ctx context.Context, tx pgx.Tx, now time.Time, environment string) (AcceptancePlatformHealthState, AcceptanceProductionInfrastructureState, error) {
|
|
state := AcceptancePlatformHealthState{State: "MISSING"}
|
|
var observedAt, receivedAt time.Time
|
|
var checksJSON []byte
|
|
err := tx.QueryRow(ctx, `
|
|
SELECT status,observed_at,received_at,checks
|
|
FROM platform_health_snapshot ORDER BY observed_at DESC,id DESC LIMIT 1`).Scan(
|
|
&state.Status, &observedAt, &receivedAt, &checksJSON,
|
|
)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return state, evaluateAcceptanceProductionInfrastructure(environment, nil), nil
|
|
}
|
|
if err != nil {
|
|
return AcceptancePlatformHealthState{}, AcceptanceProductionInfrastructureState{}, err
|
|
}
|
|
var checks []PlatformHealthCheck
|
|
if err = json.Unmarshal(checksJSON, &checks); err != nil {
|
|
return AcceptancePlatformHealthState{}, AcceptanceProductionInfrastructureState{}, err
|
|
}
|
|
return evaluateAcceptancePlatformHealth(state.Status, observedAt, receivedAt, checks, now), evaluateAcceptanceProductionInfrastructure(environment, checks), nil
|
|
}
|
|
|
|
func evaluateAcceptancePlatformHealth(status string, observedAt, receivedAt time.Time, checks []PlatformHealthCheck, now time.Time) AcceptancePlatformHealthState {
|
|
state := AcceptancePlatformHealthState{
|
|
State: "CURRENT", Status: status, ObservedAt: &observedAt, ReceivedAt: &receivedAt, CheckCount: len(checks),
|
|
}
|
|
for _, check := range checks {
|
|
switch check.Status {
|
|
case "warning":
|
|
state.WarningCount++
|
|
case "critical":
|
|
state.CriticalCount++
|
|
}
|
|
}
|
|
if observedAt.Before(now.Add(-platformHealthMaxAge)) || observedAt.After(now.Add(platformHealthFuture)) {
|
|
state.State = "STALE"
|
|
}
|
|
state.GateSatisfied = state.State == "CURRENT" && state.Status == "healthy" && state.WarningCount == 0 && state.CriticalCount == 0
|
|
return state
|
|
}
|
|
|
|
func acceptancePlatformHealthSummary(state AcceptancePlatformHealthState) string {
|
|
switch state.State {
|
|
case "MISSING":
|
|
return "尚未收到云端主机健康快照"
|
|
case "STALE":
|
|
return "云端主机健康快照必须在15分钟内更新"
|
|
}
|
|
if state.Status != "healthy" || state.WarningCount > 0 || state.CriticalCount > 0 {
|
|
return fmt.Sprintf("云端主机存在%d项预警和%d项严重异常", state.WarningCount, state.CriticalCount)
|
|
}
|
|
return fmt.Sprintf("云端主机健康快照当前有效,共%d项检查", state.CheckCount)
|
|
}
|
|
|
|
func evaluateAcceptanceProductionInfrastructure(environment string, checks []PlatformHealthCheck) AcceptanceProductionInfrastructureState {
|
|
state := AcceptanceProductionInfrastructureState{Applicable: environment == "PRODUCTION", DeploymentProfile: "MISSING"}
|
|
if !state.Applicable {
|
|
state.GateSatisfied = true
|
|
return state
|
|
}
|
|
profileReady := false
|
|
for _, check := range checks {
|
|
switch check.Name {
|
|
case "deployment_profile":
|
|
state.DeploymentProfile = check.Value
|
|
profileReady = check.Status == "healthy" && check.Value == "production"
|
|
case "offsite_backup":
|
|
state.OffsiteBackupReady = check.Status == "healthy" && strings.HasPrefix(check.Value, "age_seconds=")
|
|
case "external_alert_receiver":
|
|
state.ExternalAlertReceiverReady = check.Status == "healthy" && check.Value == "configured"
|
|
case "mock_services":
|
|
state.MockServicesDisabled = check.Status == "healthy" && check.Value == "disabled"
|
|
}
|
|
}
|
|
state.GateSatisfied = profileReady && state.OffsiteBackupReady && state.ExternalAlertReceiverReady && state.MockServicesDisabled
|
|
return state
|
|
}
|
|
|
|
func acceptanceProductionInfrastructureSummary(state AcceptanceProductionInfrastructureState) string {
|
|
if !state.Applicable {
|
|
return "非生产候选不适用生产基础设施强制门禁"
|
|
}
|
|
missing := make([]string, 0, 4)
|
|
if state.DeploymentProfile != "production" {
|
|
missing = append(missing, "生产部署模式")
|
|
}
|
|
if !state.OffsiteBackupReady {
|
|
missing = append(missing, "已验证异地备份")
|
|
}
|
|
if !state.ExternalAlertReceiverReady {
|
|
missing = append(missing, "真实外部告警接收器")
|
|
}
|
|
if !state.MockServicesDisabled {
|
|
missing = append(missing, "关闭模拟服务")
|
|
}
|
|
if len(missing) > 0 {
|
|
return "生产基础设施尚缺:" + strings.Join(missing, "、")
|
|
}
|
|
return "生产部署模式、异地备份、外部告警和模拟服务退出均已确认"
|
|
}
|
|
|
|
func acceptanceRuntimeSummary(environment string) string {
|
|
if environment == "LAB" {
|
|
return "实验室环境允许开发构建,但候选版本与镜像摘要必须明确"
|
|
}
|
|
return "预发布和生产环境的运行版本必须等于候选版本,并包含提交与构建时间"
|
|
}
|