mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-12 01:19:40 +08:00
562 lines
20 KiB
Go
562 lines
20 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"GoNavi-Wails/internal/ai"
|
|
aiservice "GoNavi-Wails/internal/ai/service"
|
|
"GoNavi-Wails/internal/connection"
|
|
"GoNavi-Wails/internal/secretstore"
|
|
)
|
|
|
|
type securityUpdateNormalizedPreview struct {
|
|
SourceType SecurityUpdateSourceType `json:"sourceType"`
|
|
ConnectionIDs []string `json:"connectionIds"`
|
|
HasGlobalProxy bool `json:"hasGlobalProxy"`
|
|
AIProviderIDs []string `json:"aiProviderIds"`
|
|
AIProvidersNeedingAttention []string `json:"aiProvidersNeedingAttention,omitempty"`
|
|
}
|
|
|
|
func (a *App) GetSecurityUpdateStatus() (SecurityUpdateStatus, error) {
|
|
a.updateMu.Lock()
|
|
defer a.updateMu.Unlock()
|
|
|
|
repo := newSecurityUpdateStateRepository(a.configDir)
|
|
status, err := repo.LoadMarker()
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
inspection, inspectErr := aiservice.NewProviderConfigStore(a.configDir, a.secretStore).Inspect()
|
|
if inspectErr != nil {
|
|
return SecurityUpdateStatus{}, inspectErr
|
|
}
|
|
if len(inspection.ProvidersNeedingMigration) > 0 {
|
|
return buildSecurityUpdatePendingStatusFromInspection(inspection, SecurityUpdateOverallStatusPending), nil
|
|
}
|
|
return SecurityUpdateStatus{
|
|
SchemaVersion: securityUpdateSchemaVersion,
|
|
OverallStatus: SecurityUpdateOverallStatusNotDetected,
|
|
Summary: SecurityUpdateSummary{},
|
|
Issues: []SecurityUpdateIssue{},
|
|
}, nil
|
|
}
|
|
return SecurityUpdateStatus{}, err
|
|
}
|
|
return status, nil
|
|
}
|
|
|
|
func (a *App) StartSecurityUpdate(request StartSecurityUpdateRequest) (SecurityUpdateStatus, error) {
|
|
a.updateMu.Lock()
|
|
defer a.updateMu.Unlock()
|
|
|
|
repo := newSecurityUpdateStateRepository(a.configDir)
|
|
status, err := repo.StartRound(request)
|
|
if err != nil {
|
|
return SecurityUpdateStatus{}, err
|
|
}
|
|
return a.executeSecurityUpdateRound(repo, status, request.SourceType, request.RawPayload)
|
|
}
|
|
|
|
func (a *App) RetrySecurityUpdateCurrentRound(request RetrySecurityUpdateRequest) (SecurityUpdateStatus, error) {
|
|
a.updateMu.Lock()
|
|
defer a.updateMu.Unlock()
|
|
|
|
repo := newSecurityUpdateStateRepository(a.configDir)
|
|
status, err := repo.RetryRound(request)
|
|
if err != nil {
|
|
return SecurityUpdateStatus{}, err
|
|
}
|
|
|
|
previewData, err := os.ReadFile(filepath.Join(status.BackupPath, securityUpdateNormalizedPreviewFileName))
|
|
if err != nil {
|
|
failed := newSecurityUpdateSystemFailureStatus(status, SecurityUpdateIssueReasonCodeEnvironmentBlocked, err)
|
|
_ = repo.WriteResult(failed)
|
|
return failed, nil
|
|
}
|
|
|
|
var preview securityUpdateNormalizedPreview
|
|
if err := json.Unmarshal(previewData, &preview); err != nil {
|
|
failed := newSecurityUpdateSystemFailureStatus(status, SecurityUpdateIssueReasonCodeValidationFailed, err)
|
|
_ = repo.WriteResult(failed)
|
|
return failed, nil
|
|
}
|
|
|
|
finalStatus, execErr := a.validateSecurityUpdateCurrentAppRound(status, preview)
|
|
if execErr != nil {
|
|
_ = repo.WriteResult(finalStatus)
|
|
return finalStatus, nil
|
|
}
|
|
if err := repo.WriteResult(finalStatus); err != nil {
|
|
return SecurityUpdateStatus{}, err
|
|
}
|
|
return finalStatus, nil
|
|
}
|
|
|
|
func (a *App) RestartSecurityUpdate(request RestartSecurityUpdateRequest) (SecurityUpdateStatus, error) {
|
|
a.updateMu.Lock()
|
|
defer a.updateMu.Unlock()
|
|
|
|
repo := newSecurityUpdateStateRepository(a.configDir)
|
|
status, err := repo.RestartRound(request)
|
|
if err != nil {
|
|
return SecurityUpdateStatus{}, err
|
|
}
|
|
return a.executeSecurityUpdateRound(repo, status, request.SourceType, request.RawPayload)
|
|
}
|
|
|
|
func (a *App) DismissSecurityUpdateReminder() (SecurityUpdateStatus, error) {
|
|
a.updateMu.Lock()
|
|
defer a.updateMu.Unlock()
|
|
|
|
now := nowRFC3339()
|
|
repo := newSecurityUpdateStateRepository(a.configDir)
|
|
status, err := repo.LoadMarker()
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return SecurityUpdateStatus{}, err
|
|
}
|
|
inspection, inspectErr := aiservice.NewProviderConfigStore(a.configDir, a.secretStore).Inspect()
|
|
if inspectErr != nil {
|
|
return SecurityUpdateStatus{}, inspectErr
|
|
}
|
|
if len(inspection.ProvidersNeedingMigration) > 0 {
|
|
status = buildSecurityUpdatePendingStatusFromInspection(inspection, SecurityUpdateOverallStatusPostponed)
|
|
} else {
|
|
status = SecurityUpdateStatus{
|
|
SchemaVersion: securityUpdateSchemaVersion,
|
|
SourceType: SecurityUpdateSourceTypeCurrentAppSavedConfig,
|
|
Summary: SecurityUpdateSummary{},
|
|
Issues: []SecurityUpdateIssue{},
|
|
}
|
|
}
|
|
}
|
|
status.SchemaVersion = securityUpdateSchemaVersion
|
|
if strings.TrimSpace(string(status.SourceType)) == "" {
|
|
status.SourceType = SecurityUpdateSourceTypeCurrentAppSavedConfig
|
|
}
|
|
if status.Issues == nil {
|
|
status.Issues = []SecurityUpdateIssue{}
|
|
}
|
|
if status.OverallStatus == SecurityUpdateOverallStatusCompleted || status.OverallStatus == SecurityUpdateOverallStatusRolledBack {
|
|
return status, nil
|
|
}
|
|
status.OverallStatus = SecurityUpdateOverallStatusPostponed
|
|
status.PostponedAt = now
|
|
status.UpdatedAt = now
|
|
|
|
if err := repo.WriteResult(status); err != nil {
|
|
return SecurityUpdateStatus{}, err
|
|
}
|
|
return repo.LoadMarker()
|
|
}
|
|
|
|
func (a *App) executeSecurityUpdateRound(repo *securityUpdateStateRepository, round SecurityUpdateStatus, sourceType SecurityUpdateSourceType, rawPayload string) (SecurityUpdateStatus, error) {
|
|
if strings.TrimSpace(string(sourceType)) == "" {
|
|
sourceType = SecurityUpdateSourceTypeCurrentAppSavedConfig
|
|
}
|
|
if sourceType != SecurityUpdateSourceTypeCurrentAppSavedConfig {
|
|
failed := newSecurityUpdateSystemFailureStatus(round, SecurityUpdateIssueReasonCodeValidationFailed, fmt.Errorf("unsupported source type: %s", sourceType))
|
|
_ = repo.WriteResult(failed)
|
|
return failed, nil
|
|
}
|
|
|
|
source, rawParsed, err := parseSecurityUpdateCurrentAppSource(rawPayload)
|
|
if err != nil {
|
|
failed := newSecurityUpdateSystemFailureStatus(round, SecurityUpdateIssueReasonCodeValidationFailed, err)
|
|
_ = repo.WriteResult(failed)
|
|
return failed, nil
|
|
}
|
|
|
|
rollbackSnapshot, err := captureSecurityUpdateCurrentAppRollbackSnapshot(a, source)
|
|
if err != nil {
|
|
failed := newSecurityUpdateSystemFailureStatus(round, securityUpdateFailureReasonForError(err), err)
|
|
_ = repo.WriteResult(failed)
|
|
return failed, nil
|
|
}
|
|
|
|
if err := securityUpdateWriteJSONFile(filepath.Join(round.BackupPath, securityUpdateSourceCurrentAppFileName), rawParsed); err != nil {
|
|
return SecurityUpdateStatus{}, err
|
|
}
|
|
|
|
finalStatus, preview, execErr := a.runSecurityUpdateCurrentAppRound(round, source)
|
|
if previewErr := securityUpdateWriteJSONFile(filepath.Join(round.BackupPath, securityUpdateNormalizedPreviewFileName), preview); previewErr != nil {
|
|
return a.rollbackSecurityUpdatePersistenceFailure(repo, rollbackSnapshot, finalStatus, previewErr)
|
|
}
|
|
|
|
if execErr != nil {
|
|
if rollbackErr := rollbackSnapshot.restore(a); rollbackErr != nil {
|
|
failed := newSecurityUpdateSystemFailureStatus(finalStatus, securityUpdateFailureReasonForError(rollbackErr), rollbackErr)
|
|
_ = repo.WriteResult(failed)
|
|
return failed, nil
|
|
}
|
|
_ = repo.WriteResult(finalStatus)
|
|
return finalStatus, nil
|
|
}
|
|
if err := repo.WriteResult(finalStatus); err != nil {
|
|
return a.rollbackSecurityUpdatePersistenceFailure(repo, rollbackSnapshot, finalStatus, err)
|
|
}
|
|
return finalStatus, nil
|
|
}
|
|
|
|
func (a *App) rollbackSecurityUpdatePersistenceFailure(
|
|
repo *securityUpdateStateRepository,
|
|
rollbackSnapshot securityUpdateCurrentAppRollbackSnapshot,
|
|
base SecurityUpdateStatus,
|
|
cause error,
|
|
) (SecurityUpdateStatus, error) {
|
|
if rollbackErr := rollbackSnapshot.restore(a); rollbackErr != nil {
|
|
failed := newSecurityUpdateSystemFailureStatus(base, securityUpdateFailureReasonForError(rollbackErr), rollbackErr)
|
|
_ = repo.WriteResult(failed)
|
|
return failed, nil
|
|
}
|
|
|
|
failed := newSecurityUpdateSystemFailureStatus(base, SecurityUpdateIssueReasonCodeEnvironmentBlocked, cause)
|
|
_ = repo.WriteResult(failed)
|
|
return failed, nil
|
|
}
|
|
|
|
func (a *App) runSecurityUpdateCurrentAppRound(round SecurityUpdateStatus, source securityUpdateCurrentAppSource) (SecurityUpdateStatus, securityUpdateNormalizedPreview, error) {
|
|
finalStatus := newSecurityUpdateRoundBaseStatus(round, SecurityUpdateSourceTypeCurrentAppSavedConfig)
|
|
|
|
preview := securityUpdateNormalizedPreview{
|
|
SourceType: SecurityUpdateSourceTypeCurrentAppSavedConfig,
|
|
ConnectionIDs: make([]string, 0, len(source.Connections)),
|
|
HasGlobalProxy: source.GlobalProxy != nil,
|
|
AIProviderIDs: []string{},
|
|
}
|
|
|
|
connectionRepo := a.savedConnectionRepository()
|
|
for _, item := range source.Connections {
|
|
finalStatus.Summary.Total++
|
|
preview.ConnectionIDs = append(preview.ConnectionIDs, item.ID)
|
|
if _, err := connectionRepo.Save(connection.SavedConnectionInput(item)); err != nil {
|
|
failed := newSecurityUpdateSystemFailureStatus(finalStatus, SecurityUpdateIssueReasonCodeEnvironmentBlocked, err)
|
|
return failed, preview, err
|
|
}
|
|
finalStatus.Summary.Updated++
|
|
}
|
|
|
|
if source.GlobalProxy != nil {
|
|
finalStatus.Summary.Total++
|
|
if _, err := a.saveGlobalProxy(connection.SaveGlobalProxyInput(*source.GlobalProxy)); err != nil {
|
|
failed := newSecurityUpdateSystemFailureStatus(finalStatus, SecurityUpdateIssueReasonCodeEnvironmentBlocked, err)
|
|
return failed, preview, err
|
|
}
|
|
finalStatus.Summary.Updated++
|
|
}
|
|
|
|
providerSnapshot, err := aiservice.NewProviderConfigStore(a.configDir, a.secretStore).Load()
|
|
if err != nil {
|
|
failed := newSecurityUpdateSystemFailureStatus(finalStatus, securityUpdateFailureReasonForError(err), err)
|
|
return failed, preview, err
|
|
}
|
|
|
|
for _, provider := range providerSnapshot.Providers {
|
|
if !providerParticipatesInSecurityUpdate(provider) {
|
|
continue
|
|
}
|
|
|
|
preview.AIProviderIDs = append(preview.AIProviderIDs, provider.ID)
|
|
finalStatus.Summary.Total++
|
|
if provider.HasSecret && strings.TrimSpace(provider.APIKey) == "" {
|
|
finalStatus.OverallStatus = SecurityUpdateOverallStatusNeedsAttention
|
|
finalStatus.Summary.Pending++
|
|
finalStatus.Issues = append(finalStatus.Issues, SecurityUpdateIssue{
|
|
ID: "ai-provider-" + provider.ID,
|
|
Scope: SecurityUpdateIssueScopeAIProvider,
|
|
RefID: provider.ID,
|
|
Title: provider.Name,
|
|
Severity: SecurityUpdateIssueSeverityMedium,
|
|
Status: SecurityUpdateItemStatusNeedsAttention,
|
|
ReasonCode: SecurityUpdateIssueReasonCodeSecretMissing,
|
|
Action: SecurityUpdateIssueActionOpenAISettings,
|
|
Message: "AI 提供商配置需要补充后才能完成安全更新",
|
|
})
|
|
preview.AIProvidersNeedingAttention = append(preview.AIProvidersNeedingAttention, provider.ID)
|
|
continue
|
|
}
|
|
finalStatus.Summary.Updated++
|
|
}
|
|
|
|
if finalStatus.OverallStatus == SecurityUpdateOverallStatusCompleted {
|
|
finalStatus.CompletedAt = finalStatus.UpdatedAt
|
|
}
|
|
|
|
return finalStatus, preview, nil
|
|
}
|
|
|
|
func (a *App) validateSecurityUpdateCurrentAppRound(round SecurityUpdateStatus, preview securityUpdateNormalizedPreview) (SecurityUpdateStatus, error) {
|
|
if strings.TrimSpace(string(preview.SourceType)) == "" {
|
|
preview.SourceType = SecurityUpdateSourceTypeCurrentAppSavedConfig
|
|
}
|
|
|
|
finalStatus := newSecurityUpdateRoundBaseStatus(round, preview.SourceType)
|
|
connectionRepo := a.savedConnectionRepository()
|
|
for _, id := range preview.ConnectionIDs {
|
|
finalStatus.Summary.Total++
|
|
savedConnection, err := connectionRepo.Find(id)
|
|
if err != nil {
|
|
markSecurityUpdateNeedsAttention(
|
|
&finalStatus,
|
|
SecurityUpdateIssue{
|
|
ID: "connection-" + id,
|
|
Scope: SecurityUpdateIssueScopeConnection,
|
|
RefID: id,
|
|
Title: id,
|
|
Severity: SecurityUpdateIssueSeverityMedium,
|
|
Status: SecurityUpdateItemStatusNeedsAttention,
|
|
ReasonCode: SecurityUpdateIssueReasonCodeValidationFailed,
|
|
Action: SecurityUpdateIssueActionOpenConnection,
|
|
Message: "连接配置已不存在或仍需重新保存后才能完成安全更新",
|
|
},
|
|
)
|
|
continue
|
|
}
|
|
if _, err := a.resolveConnectionSecrets(savedConnection.Config); err != nil {
|
|
if secretstore.IsUnavailable(err) {
|
|
failed := newSecurityUpdateSystemFailureStatus(finalStatus, SecurityUpdateIssueReasonCodeEnvironmentBlocked, err)
|
|
return failed, err
|
|
}
|
|
reason := SecurityUpdateIssueReasonCodeValidationFailed
|
|
message := "连接配置仍需补充后才能完成安全更新"
|
|
if os.IsNotExist(err) {
|
|
reason = SecurityUpdateIssueReasonCodeSecretMissing
|
|
message = "连接密码已丢失,请重新保存后再继续"
|
|
}
|
|
markSecurityUpdateNeedsAttention(
|
|
&finalStatus,
|
|
SecurityUpdateIssue{
|
|
ID: "connection-" + id,
|
|
Scope: SecurityUpdateIssueScopeConnection,
|
|
RefID: id,
|
|
Title: savedConnection.Name,
|
|
Severity: SecurityUpdateIssueSeverityMedium,
|
|
Status: SecurityUpdateItemStatusNeedsAttention,
|
|
ReasonCode: reason,
|
|
Action: SecurityUpdateIssueActionOpenConnection,
|
|
Message: message,
|
|
},
|
|
)
|
|
continue
|
|
}
|
|
finalStatus.Summary.Updated++
|
|
}
|
|
|
|
if preview.HasGlobalProxy {
|
|
finalStatus.Summary.Total++
|
|
proxyView, err := a.loadStoredGlobalProxyView()
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
failed := newSecurityUpdateSystemFailureStatus(finalStatus, securityUpdateFailureReasonForError(err), err)
|
|
return failed, err
|
|
}
|
|
markSecurityUpdateNeedsAttention(
|
|
&finalStatus,
|
|
SecurityUpdateIssue{
|
|
ID: "global-proxy-default",
|
|
Scope: SecurityUpdateIssueScopeGlobalProxy,
|
|
Title: "全局代理",
|
|
Severity: SecurityUpdateIssueSeverityMedium,
|
|
Status: SecurityUpdateItemStatusNeedsAttention,
|
|
ReasonCode: SecurityUpdateIssueReasonCodeValidationFailed,
|
|
Action: SecurityUpdateIssueActionOpenProxySettings,
|
|
Message: "全局代理配置已不存在或仍需重新保存后才能完成安全更新",
|
|
},
|
|
)
|
|
} else {
|
|
if proxyView.HasPassword {
|
|
if _, err := a.loadGlobalProxySecretBundle(proxyView); err != nil {
|
|
if secretstore.IsUnavailable(err) {
|
|
failed := newSecurityUpdateSystemFailureStatus(finalStatus, SecurityUpdateIssueReasonCodeEnvironmentBlocked, err)
|
|
return failed, err
|
|
}
|
|
reason := SecurityUpdateIssueReasonCodeValidationFailed
|
|
message := "全局代理密码仍需补充后才能完成安全更新"
|
|
if os.IsNotExist(err) {
|
|
reason = SecurityUpdateIssueReasonCodeSecretMissing
|
|
message = "全局代理密码已丢失,请重新保存后再继续"
|
|
}
|
|
markSecurityUpdateNeedsAttention(
|
|
&finalStatus,
|
|
SecurityUpdateIssue{
|
|
ID: "global-proxy-default",
|
|
Scope: SecurityUpdateIssueScopeGlobalProxy,
|
|
Title: "全局代理",
|
|
Severity: SecurityUpdateIssueSeverityMedium,
|
|
Status: SecurityUpdateItemStatusNeedsAttention,
|
|
ReasonCode: reason,
|
|
Action: SecurityUpdateIssueActionOpenProxySettings,
|
|
Message: message,
|
|
},
|
|
)
|
|
goto validateProviders
|
|
}
|
|
}
|
|
finalStatus.Summary.Updated++
|
|
}
|
|
}
|
|
|
|
validateProviders:
|
|
providerSnapshot, err := aiservice.NewProviderConfigStore(a.configDir, a.secretStore).Load()
|
|
if err != nil {
|
|
failed := newSecurityUpdateSystemFailureStatus(finalStatus, securityUpdateFailureReasonForError(err), err)
|
|
return failed, err
|
|
}
|
|
|
|
providersByID := make(map[string]ai.ProviderConfig, len(providerSnapshot.Providers))
|
|
for _, provider := range providerSnapshot.Providers {
|
|
providersByID[provider.ID] = provider
|
|
}
|
|
|
|
for _, providerID := range preview.AIProviderIDs {
|
|
finalStatus.Summary.Total++
|
|
provider, ok := providersByID[providerID]
|
|
if !ok {
|
|
markSecurityUpdateNeedsAttention(
|
|
&finalStatus,
|
|
SecurityUpdateIssue{
|
|
ID: "ai-provider-" + providerID,
|
|
Scope: SecurityUpdateIssueScopeAIProvider,
|
|
RefID: providerID,
|
|
Title: providerID,
|
|
Severity: SecurityUpdateIssueSeverityMedium,
|
|
Status: SecurityUpdateItemStatusNeedsAttention,
|
|
ReasonCode: SecurityUpdateIssueReasonCodeValidationFailed,
|
|
Action: SecurityUpdateIssueActionOpenAISettings,
|
|
Message: "AI 提供商配置已不存在或仍需重新保存后才能完成安全更新",
|
|
},
|
|
)
|
|
continue
|
|
}
|
|
if provider.HasSecret && strings.TrimSpace(provider.APIKey) == "" {
|
|
markSecurityUpdateNeedsAttention(
|
|
&finalStatus,
|
|
SecurityUpdateIssue{
|
|
ID: "ai-provider-" + provider.ID,
|
|
Scope: SecurityUpdateIssueScopeAIProvider,
|
|
RefID: provider.ID,
|
|
Title: provider.Name,
|
|
Severity: SecurityUpdateIssueSeverityMedium,
|
|
Status: SecurityUpdateItemStatusNeedsAttention,
|
|
ReasonCode: SecurityUpdateIssueReasonCodeSecretMissing,
|
|
Action: SecurityUpdateIssueActionOpenAISettings,
|
|
Message: "AI 提供商配置需要补充后才能完成安全更新",
|
|
},
|
|
)
|
|
continue
|
|
}
|
|
finalStatus.Summary.Updated++
|
|
}
|
|
|
|
if finalStatus.OverallStatus == SecurityUpdateOverallStatusCompleted {
|
|
finalStatus.CompletedAt = finalStatus.UpdatedAt
|
|
}
|
|
return finalStatus, nil
|
|
}
|
|
|
|
func providerParticipatesInSecurityUpdate(provider ai.ProviderConfig) bool {
|
|
return provider.HasSecret || strings.TrimSpace(provider.APIKey) != ""
|
|
}
|
|
|
|
func buildSecurityUpdatePendingStatusFromInspection(
|
|
inspection aiservice.ProviderConfigStoreInspection,
|
|
overallStatus SecurityUpdateOverallStatus,
|
|
) SecurityUpdateStatus {
|
|
providersByID := make(map[string]ai.ProviderConfig, len(inspection.Snapshot.Providers))
|
|
for _, provider := range inspection.Snapshot.Providers {
|
|
providersByID[provider.ID] = provider
|
|
}
|
|
|
|
issues := make([]SecurityUpdateIssue, 0, len(inspection.ProvidersNeedingMigration))
|
|
for _, providerID := range inspection.ProvidersNeedingMigration {
|
|
provider := providersByID[providerID]
|
|
title := strings.TrimSpace(provider.Name)
|
|
if title == "" {
|
|
title = providerID
|
|
}
|
|
issues = append(issues, SecurityUpdateIssue{
|
|
ID: "ai-provider-" + providerID,
|
|
Scope: SecurityUpdateIssueScopeAIProvider,
|
|
RefID: providerID,
|
|
Title: title,
|
|
Severity: SecurityUpdateIssueSeverityMedium,
|
|
Status: SecurityUpdateItemStatusPending,
|
|
ReasonCode: SecurityUpdateIssueReasonCodeMigrationRequired,
|
|
Action: SecurityUpdateIssueActionOpenAISettings,
|
|
Message: "AI 提供商配置仍保存在当前应用配置中,完成安全更新后会迁入新的安全存储。",
|
|
})
|
|
}
|
|
|
|
return SecurityUpdateStatus{
|
|
SchemaVersion: securityUpdateSchemaVersion,
|
|
OverallStatus: overallStatus,
|
|
SourceType: SecurityUpdateSourceTypeCurrentAppSavedConfig,
|
|
ReminderVisible: overallStatus == SecurityUpdateOverallStatusPending,
|
|
CanStart: overallStatus == SecurityUpdateOverallStatusPending || overallStatus == SecurityUpdateOverallStatusPostponed,
|
|
CanPostpone: overallStatus == SecurityUpdateOverallStatusPending || overallStatus == SecurityUpdateOverallStatusPostponed,
|
|
Summary: SecurityUpdateSummary{
|
|
Total: len(issues),
|
|
Pending: len(issues),
|
|
},
|
|
Issues: issues,
|
|
}
|
|
}
|
|
|
|
func newSecurityUpdateRoundBaseStatus(round SecurityUpdateStatus, sourceType SecurityUpdateSourceType) SecurityUpdateStatus {
|
|
if strings.TrimSpace(string(sourceType)) == "" {
|
|
sourceType = SecurityUpdateSourceTypeCurrentAppSavedConfig
|
|
}
|
|
return SecurityUpdateStatus{
|
|
SchemaVersion: securityUpdateSchemaVersion,
|
|
MigrationID: round.MigrationID,
|
|
OverallStatus: SecurityUpdateOverallStatusCompleted,
|
|
SourceType: sourceType,
|
|
BackupAvailable: round.BackupAvailable || strings.TrimSpace(round.BackupPath) != "",
|
|
BackupPath: round.BackupPath,
|
|
StartedAt: round.StartedAt,
|
|
UpdatedAt: nowRFC3339(),
|
|
Summary: SecurityUpdateSummary{},
|
|
Issues: []SecurityUpdateIssue{},
|
|
}
|
|
}
|
|
|
|
func markSecurityUpdateNeedsAttention(status *SecurityUpdateStatus, issue SecurityUpdateIssue) {
|
|
status.OverallStatus = SecurityUpdateOverallStatusNeedsAttention
|
|
status.Summary.Pending++
|
|
status.Issues = append(status.Issues, issue)
|
|
}
|
|
|
|
func securityUpdateFailureReasonForError(err error) SecurityUpdateIssueReasonCode {
|
|
if secretstore.IsUnavailable(err) {
|
|
return SecurityUpdateIssueReasonCodeEnvironmentBlocked
|
|
}
|
|
return SecurityUpdateIssueReasonCodeValidationFailed
|
|
}
|
|
|
|
func newSecurityUpdateSystemFailureStatus(base SecurityUpdateStatus, reasonCode SecurityUpdateIssueReasonCode, err error) SecurityUpdateStatus {
|
|
status := base
|
|
status.SchemaVersion = securityUpdateSchemaVersion
|
|
status.OverallStatus = SecurityUpdateOverallStatusRolledBack
|
|
status.BackupAvailable = status.BackupAvailable || strings.TrimSpace(status.BackupPath) != ""
|
|
status.UpdatedAt = nowRFC3339()
|
|
status.CompletedAt = ""
|
|
status.LastError = err.Error()
|
|
status.Summary.Failed++
|
|
status.Issues = []SecurityUpdateIssue{
|
|
{
|
|
ID: "system-blocked",
|
|
Scope: SecurityUpdateIssueScopeSystem,
|
|
Title: "安全更新未完成",
|
|
Severity: SecurityUpdateIssueSeverityHigh,
|
|
Status: SecurityUpdateItemStatusFailed,
|
|
ReasonCode: reasonCode,
|
|
Action: SecurityUpdateIssueActionViewDetails,
|
|
Message: "当前环境无法完成本次安全更新,请稍后重试",
|
|
},
|
|
}
|
|
return status
|
|
}
|