Files
sub2api-cn-relay-manager/internal/store/sqlite/provider_accounts_sync.go

262 lines
8.5 KiB
Go
Raw Normal View History

package sqlite
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strings"
"time"
)
const providerAccountDeprecatedMissingReason = "missing_from_latest_batch"
func SyncProviderAccountsFromLatestImportBatches(ctx context.Context, store *DB) error {
if store == nil {
return fmt.Errorf("store is required")
}
batches, err := store.ImportBatches().ListLatestReconcilable(ctx)
if err != nil {
return err
}
for _, batch := range batches {
if err := SyncProviderAccountsFromImportBatch(ctx, store, batch.ID); err != nil {
return err
}
}
return nil
}
func SyncProviderAccountsFromImportBatch(ctx context.Context, store *DB, batchID int64) error {
if store == nil {
return fmt.Errorf("store is required")
}
if batchID <= 0 {
return fmt.Errorf("batch_id is required")
}
batch, err := store.ImportBatches().GetByID(ctx, batchID)
if err != nil {
return fmt.Errorf("get import batch %d: %w", batchID, err)
}
switch strings.TrimSpace(batch.BatchStatus) {
case "succeeded", "partially_succeeded":
default:
return nil
}
resources, err := store.ManagedResources().GetByBatchID(ctx, batchID)
if err != nil {
return fmt.Errorf("get managed resources for batch %d: %w", batchID, err)
}
items, err := store.ImportBatchItems().GetByBatchID(ctx, batchID)
if err != nil {
return fmt.Errorf("get import batch items for batch %d: %w", batchID, err)
}
nowText := time.Now().UTC().Format(time.RFC3339)
shadowGroupID := ""
shadowHostID := ""
for _, resource := range resources {
if strings.TrimSpace(resource.ResourceType) == "group" {
shadowGroupID = strings.TrimSpace(resource.HostResourceID)
break
}
}
hostRow, err := store.Hosts().GetByID(ctx, batch.HostID)
if err == nil {
shadowHostID = strings.TrimSpace(hostRow.HostID)
}
matchedRoute, routeErr := resolveProviderAccountRouteBinding(ctx, store, shadowHostID, shadowGroupID)
if routeErr != nil && routeErr != sql.ErrNoRows {
return routeErr
}
accountResources := make([]ManagedResource, 0)
for _, resource := range resources {
if strings.TrimSpace(resource.ResourceType) == "account" {
accountResources = append(accountResources, resource)
}
}
itemByAccountID, unmatchedItems := indexBatchItemsByAccountID(items)
keepAccountIDs := make([]string, 0, len(accountResources))
for index, resource := range accountResources {
hostAccountID := strings.TrimSpace(resource.HostResourceID)
if hostAccountID == "" {
continue
}
keepAccountIDs = append(keepAccountIDs, hostAccountID)
match, ok := itemByAccountID[hostAccountID]
if !ok && index < len(unmatchedItems) {
match = unmatchedItems[index]
}
accountStatus, probeStatus := deriveProviderAccountState(batch, len(accountResources), match)
row := ProviderAccount{
HostID: batch.HostID,
ProviderID: batch.ProviderID,
RouteID: matchedRoute.RouteID,
ShadowGroupID: shadowGroupID,
HostAccountID: hostAccountID,
KeyFingerprint: fallbackString(match.KeyFingerprint, "legacy:"+hostAccountID),
AccountName: fallbackString(resource.ResourceName, hostAccountID),
AccountStatus: accountStatus,
LastProbeStatus: probeStatus,
LastProbeAt: nowText,
}
if existing, err := store.ProviderAccounts().GetByHostIDAndAccountID(ctx, batch.HostID, hostAccountID); err == nil {
if strings.TrimSpace(existing.RouteID) != "" {
row.RouteID = existing.RouteID
}
if strings.TrimSpace(existing.ShadowGroupID) != "" {
row.ShadowGroupID = existing.ShadowGroupID
}
preserveManagedProviderAccountStatus(&row, existing)
}
if _, err := store.ProviderAccounts().Upsert(ctx, row); err != nil {
return fmt.Errorf("upsert provider account %q from batch %d: %w", hostAccountID, batchID, err)
}
}
if err := store.ProviderAccounts().DeprecateMissingForScope(ctx, batch.ProviderID, batch.HostID, keepAccountIDs, providerAccountDeprecatedMissingReason); err != nil {
return err
}
return nil
}
func resolveProviderAccountRouteBinding(ctx context.Context, store *DB, shadowHostID, shadowGroupID string) (LogicalGroupRoute, error) {
if store == nil {
return LogicalGroupRoute{}, fmt.Errorf("store is required")
}
shadowHostID = strings.TrimSpace(shadowHostID)
shadowGroupID = strings.TrimSpace(shadowGroupID)
if shadowHostID == "" || shadowGroupID == "" {
return LogicalGroupRoute{}, sql.ErrNoRows
}
route, err := store.LogicalGroupRoutes().GetByShadowBinding(ctx, shadowHostID, shadowGroupID)
if err != nil {
if err == sql.ErrNoRows {
return LogicalGroupRoute{}, err
}
if isAmbiguousProviderAccountRouteBinding(err) {
return LogicalGroupRoute{}, sql.ErrNoRows
}
return LogicalGroupRoute{}, fmt.Errorf("resolve logical route for provider account shadow binding %q/%q: %w", shadowHostID, shadowGroupID, err)
}
return route, nil
}
type legacyBatchAccountProjection struct {
KeyFingerprint string
AccountStatus string
ValidationStatus string
ProbeStatus string
AccountID string
ProbeAdvisory bool
SmokeModelSeen bool
}
func indexBatchItemsByAccountID(items []ImportBatchItem) (map[string]legacyBatchAccountProjection, []legacyBatchAccountProjection) {
indexed := make(map[string]legacyBatchAccountProjection, len(items))
unmatched := make([]legacyBatchAccountProjection, 0, len(items))
for _, item := range items {
projection := legacyBatchAccountProjection{
KeyFingerprint: strings.TrimSpace(item.KeyFingerprint),
AccountStatus: strings.TrimSpace(item.AccountStatus),
}
var payload map[string]any
if err := json.Unmarshal([]byte(defaultJSON(strings.TrimSpace(item.ProbeSummaryJSON), "{}")), &payload); err == nil {
if value, ok := payload["probe_status"].(string); ok {
projection.ProbeStatus = strings.TrimSpace(value)
}
if value, ok := payload["account_id"].(string); ok {
projection.AccountID = strings.TrimSpace(value)
}
if value, ok := payload["validation_status"].(string); ok {
projection.ValidationStatus = strings.TrimSpace(value)
}
if value, ok := payload["probe_advisory"].(bool); ok {
projection.ProbeAdvisory = value
}
if value, ok := payload["smoke_model_seen"].(bool); ok {
projection.SmokeModelSeen = value
}
}
if projection.AccountID != "" {
indexed[projection.AccountID] = projection
continue
}
unmatched = append(unmatched, projection)
}
return indexed, unmatched
}
func providerAccountStatusFromLegacy(accountStatus string) string {
switch strings.TrimSpace(accountStatus) {
case "passed", "warning":
return ProviderAccountStatusActive
case ProviderAccountStatusDisabled:
return ProviderAccountStatusDisabled
case ProviderAccountStatusDeprecated:
return ProviderAccountStatusDeprecated
default:
return ProviderAccountStatusBroken
}
}
func deriveProviderAccountState(batch ImportBatch, accountResourceCount int, projection legacyBatchAccountProjection) (string, string) {
legacyStatus := fallbackString(projection.ValidationStatus, projection.AccountStatus)
probeStatus := strings.TrimSpace(projection.ProbeStatus)
if projection.ProbeAdvisory || strings.EqualFold(legacyStatus, "warning") {
legacyStatus = "warning"
if probeStatus == "" || strings.EqualFold(probeStatus, "failed") {
probeStatus = "warning"
}
}
if importBatchAccessReady(batch.AccessStatus) &&
accountResourceCount == 1 &&
projection.SmokeModelSeen &&
strings.EqualFold(legacyStatus, "failed") {
return ProviderAccountStatusActive, "gateway_ready"
}
return providerAccountStatusFromLegacy(legacyStatus), probeStatus
}
func importBatchAccessReady(accessStatus string) bool {
switch strings.TrimSpace(accessStatus) {
case "subscription_ready", "self_service_ready", "fully_ready":
return true
default:
return false
}
}
func fallbackString(values ...string) string {
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return trimmed
}
}
return ""
}
func preserveManagedProviderAccountStatus(row *ProviderAccount, existing ProviderAccount) {
if row == nil {
return
}
switch strings.TrimSpace(existing.AccountStatus) {
case ProviderAccountStatusDisabled:
row.AccountStatus = ProviderAccountStatusDisabled
row.DisabledReason = strings.TrimSpace(existing.DisabledReason)
case ProviderAccountStatusDeprecated:
if strings.TrimSpace(existing.DisabledReason) != providerAccountDeprecatedMissingReason {
row.AccountStatus = ProviderAccountStatusDeprecated
row.DisabledReason = strings.TrimSpace(existing.DisabledReason)
}
}
}
func isAmbiguousProviderAccountRouteBinding(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), "multiple logical group routes match shadow binding")
}