2026-05-15 19:26:25 +08:00
|
|
|
package provision
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"sub2api-cn-relay-manager/internal/store/sqlite"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type ProviderQuery struct {
|
|
|
|
|
ProviderID string
|
|
|
|
|
PackID string
|
2026-05-18 22:22:22 +08:00
|
|
|
HostID string
|
2026-05-15 19:26:25 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2026-05-18 22:22:22 +08:00
|
|
|
hostRow, batchRow, err := s.resolveHostAndBatch(ctx, provider.ID, query.HostID)
|
2026-05-15 19:26:25 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return ProviderSnapshot{}, err
|
|
|
|
|
}
|
2026-05-19 20:21:21 +08:00
|
|
|
managedResources, err := s.store.ManagedResources().GetByBatchID(ctx, batchRow.ID)
|
2026-05-15 19:26:25 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return ProviderSnapshot{}, err
|
|
|
|
|
}
|
|
|
|
|
accessClosures, err := s.store.AccessClosures().GetByBatchID(ctx, batchRow.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ProviderSnapshot{}, err
|
|
|
|
|
}
|
2026-05-20 22:09:40 +08:00
|
|
|
batches, err := s.store.ImportBatches().ListByProviderIDAndHostID(ctx, provider.ID, hostRow.ID)
|
2026-05-15 19:26:25 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return ProviderSnapshot{}, err
|
|
|
|
|
}
|
2026-05-20 22:09:40 +08:00
|
|
|
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
|
2026-05-15 19:26:25 +08:00
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-20 22:09:40 +08:00
|
|
|
providerStatus := deriveProviderStatus(batchRow.BatchStatus, latestAccessStatus, latestReconcileStatus)
|
2026-05-15 19:26:25 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 22:22:22 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 22:09:40 +08:00
|
|
|
func deriveProviderStatus(batchStatus, accessStatus, reconcileStatus string) string {
|
2026-05-25 10:48:04 +08:00
|
|
|
batchStatus = strings.TrimSpace(batchStatus)
|
2026-05-15 19:26:25 +08:00
|
|
|
reconcileStatus = strings.TrimSpace(reconcileStatus)
|
2026-05-20 22:09:40 +08:00
|
|
|
accessStatus = strings.TrimSpace(accessStatus)
|
2026-05-25 10:48:04 +08:00
|
|
|
switch batchStatus {
|
|
|
|
|
case BatchStatusFailed, BatchStatusRolledBack:
|
|
|
|
|
return ProviderStatusFailed
|
|
|
|
|
}
|
2026-05-30 14:42:51 +08:00
|
|
|
if (batchStatus == BatchStatusSucceeded || batchStatus == BatchStatusPartial) && providerAccessReady(accessStatus) {
|
2026-05-20 22:09:40 +08:00
|
|
|
return ProviderStatusActive
|
|
|
|
|
}
|
2026-05-15 19:26:25 +08:00
|
|
|
if reconcileStatus != "" && reconcileStatus != "not_run" {
|
|
|
|
|
return reconcileStatus
|
|
|
|
|
}
|
2026-05-25 10:48:04 +08:00
|
|
|
switch batchStatus {
|
2026-05-15 19:26:25 +08:00
|
|
|
case BatchStatusSucceeded:
|
|
|
|
|
return ProviderStatusActive
|
|
|
|
|
case "running":
|
|
|
|
|
return "running"
|
|
|
|
|
default:
|
|
|
|
|
return firstNonEmpty(batchStatus, "unknown")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-30 14:42:51 +08:00
|
|
|
|
|
|
|
|
func providerAccessReady(accessStatus string) bool {
|
|
|
|
|
accessStatus = strings.TrimSpace(accessStatus)
|
|
|
|
|
return accessStatus != "" && accessStatus != AccessStatusBroken
|
|
|
|
|
}
|