Files
sub2api-cn-relay-manager/internal/provision/provider_status_service.go

198 lines
6.4 KiB
Go
Raw Normal View History

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 {
batchStatus = strings.TrimSpace(batchStatus)
reconcileStatus = strings.TrimSpace(reconcileStatus)
accessStatus = strings.TrimSpace(accessStatus)
switch batchStatus {
case BatchStatusFailed, BatchStatusRolledBack:
return ProviderStatusFailed
}
if (batchStatus == BatchStatusSucceeded || batchStatus == BatchStatusPartial) && providerAccessReady(accessStatus) {
return ProviderStatusActive
}
if reconcileStatus != "" && reconcileStatus != "not_run" {
return reconcileStatus
}
switch batchStatus {
case BatchStatusSucceeded:
return ProviderStatusActive
case "running":
return "running"
default:
return firstNonEmpty(batchStatus, "unknown")
}
}
func providerAccessReady(accessStatus string) bool {
accessStatus = strings.TrimSpace(accessStatus)
return accessStatus != "" && accessStatus != AccessStatusBroken
}