package provision import ( "context" "encoding/json" "fmt" "strings" "sub2api-cn-relay-manager/internal/store/sqlite" ) type ProviderQuery struct { ProviderID string PackID string HostID string } type ProviderSnapshot struct { Host sqlite.Host Pack sqlite.Pack Provider sqlite.Provider Batch sqlite.ImportBatch ManagedResources []sqlite.ManagedResource AccessClosures []sqlite.AccessClosureRecord ReconcileRuns []sqlite.ReconcileRun ProviderStatus string LatestAccessStatus string LatestReconcileStatus string LatestReconcileSummary map[string]any } type ProviderStatusService struct { store *sqlite.DB } func NewProviderStatusService(store *sqlite.DB) *ProviderStatusService { return &ProviderStatusService{store: store} } func (s *ProviderStatusService) GetStatus(ctx context.Context, query ProviderQuery) (ProviderSnapshot, error) { return s.snapshot(ctx, query) } func (s *ProviderStatusService) GetResources(ctx context.Context, query ProviderQuery) (ProviderSnapshot, error) { return s.snapshot(ctx, query) } func (s *ProviderStatusService) snapshot(ctx context.Context, query ProviderQuery) (ProviderSnapshot, error) { if s == nil || s.store == nil { return ProviderSnapshot{}, fmt.Errorf("store is required") } provider, err := s.resolveProvider(ctx, query) if err != nil { return ProviderSnapshot{}, err } packRow, err := s.store.Packs().GetByID(ctx, provider.PackID) if err != nil { return ProviderSnapshot{}, err } hostRow, batchRow, err := s.resolveHostAndBatch(ctx, provider.ID, query.HostID) if err != nil { return ProviderSnapshot{}, err } managedResources, err := s.store.ManagedResources().GetByBatchID(ctx, batchRow.ID) if err != nil { return ProviderSnapshot{}, err } accessClosures, err := s.store.AccessClosures().GetByBatchID(ctx, batchRow.ID) if err != nil { return ProviderSnapshot{}, err } batches, err := s.store.ImportBatches().ListByProviderIDAndHostID(ctx, provider.ID, hostRow.ID) if err != nil { return ProviderSnapshot{}, err } modeStatuses, err := LatestModeAccessStatuses(ctx, s.store, batches) if err != nil { return ProviderSnapshot{}, err } latestAccessStatus := AggregateAccessStatus(modeStatuses) reconcileRuns, err := s.store.ReconcileRuns().GetByBatchID(ctx, batchRow.ID) if err != nil { return ProviderSnapshot{}, err } latestReconcileStatus := "not_run" latestReconcileSummary := map[string]any{} if len(reconcileRuns) > 0 { latestReconcileStatus = firstNonEmpty(reconcileRuns[0].Status, latestReconcileStatus) if strings.TrimSpace(reconcileRuns[0].SummaryJSON) != "" { if err := json.Unmarshal([]byte(reconcileRuns[0].SummaryJSON), &latestReconcileSummary); err != nil { return ProviderSnapshot{}, fmt.Errorf("decode reconcile summary: %w", err) } } } providerStatus := deriveProviderStatus(batchRow.BatchStatus, latestAccessStatus, latestReconcileStatus) return ProviderSnapshot{ Host: hostRow, Pack: packRow, Provider: provider, Batch: batchRow, ManagedResources: managedResources, AccessClosures: accessClosures, ReconcileRuns: reconcileRuns, ProviderStatus: providerStatus, LatestAccessStatus: latestAccessStatus, LatestReconcileStatus: latestReconcileStatus, LatestReconcileSummary: latestReconcileSummary, }, nil } func (s *ProviderStatusService) resolveProvider(ctx context.Context, query ProviderQuery) (sqlite.Provider, error) { providerID := strings.TrimSpace(query.ProviderID) packID := strings.TrimSpace(query.PackID) if providerID == "" { return sqlite.Provider{}, fmt.Errorf("provider_id is required") } if packID != "" { packRow, err := s.store.Packs().GetByPackID(ctx, packID) if err != nil { return sqlite.Provider{}, err } return s.store.Providers().GetByPackIDAndProviderID(ctx, packRow.ID, providerID) } providers, err := s.store.Providers().ListByProviderID(ctx, providerID) if err != nil { return sqlite.Provider{}, err } if len(providers) == 0 { return sqlite.Provider{}, fmt.Errorf("provider %q not found", providerID) } if len(providers) > 1 { return sqlite.Provider{}, fmt.Errorf("provider %q exists in multiple packs; pack_id is required", providerID) } return providers[0], nil } func (s *ProviderStatusService) resolveHostAndBatch(ctx context.Context, providerID int64, hostQuery string) (sqlite.Host, sqlite.ImportBatch, error) { if strings.TrimSpace(hostQuery) != "" { hostRow, err := s.store.Hosts().GetByHostID(ctx, hostQuery) if err != nil { return sqlite.Host{}, sqlite.ImportBatch{}, err } batchRow, err := s.store.ImportBatches().GetLatestByProviderIDAndHostID(ctx, providerID, hostRow.ID) if err != nil { return sqlite.Host{}, sqlite.ImportBatch{}, err } return hostRow, batchRow, nil } batches, err := s.store.ImportBatches().ListByProviderID(ctx, providerID) if err != nil { return sqlite.Host{}, sqlite.ImportBatch{}, err } if len(batches) == 0 { return sqlite.Host{}, sqlite.ImportBatch{}, fmt.Errorf("latest import batch not found for provider") } hostID := batches[0].HostID for _, batch := range batches[1:] { if batch.HostID != hostID { return sqlite.Host{}, sqlite.ImportBatch{}, fmt.Errorf("provider exists on multiple hosts; host_id is required") } } hostRow, err := s.store.Hosts().GetByID(ctx, hostID) if err != nil { return sqlite.Host{}, sqlite.ImportBatch{}, err } return hostRow, batches[0], nil } func deriveProviderStatus(batchStatus, accessStatus, reconcileStatus string) string { reconcileStatus = strings.TrimSpace(reconcileStatus) accessStatus = strings.TrimSpace(accessStatus) if strings.TrimSpace(batchStatus) == BatchStatusSucceeded && accessStatus != "" && accessStatus != AccessStatusBroken { return ProviderStatusActive } if reconcileStatus != "" && reconcileStatus != "not_run" { return reconcileStatus } switch strings.TrimSpace(batchStatus) { case BatchStatusSucceeded: return ProviderStatusActive case BatchStatusFailed: return ProviderStatusFailed case BatchStatusRolledBack: return ProviderStatusFailed case "running": return "running" default: return firstNonEmpty(batchStatus, "unknown") } }