feat(accounts): add provider account inventory api
This commit is contained in:
181
internal/store/sqlite/provider_accounts_sync.go
Normal file
181
internal/store/sqlite/provider_accounts_sync.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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 := ""
|
||||
for _, resource := range resources {
|
||||
if strings.TrimSpace(resource.ResourceType) == "group" {
|
||||
shadowGroupID = strings.TrimSpace(resource.HostResourceID)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
row := ProviderAccount{
|
||||
HostID: batch.HostID,
|
||||
ProviderID: batch.ProviderID,
|
||||
ShadowGroupID: shadowGroupID,
|
||||
HostAccountID: hostAccountID,
|
||||
KeyFingerprint: fallbackString(match.KeyFingerprint, "legacy:"+hostAccountID),
|
||||
AccountName: fallbackString(resource.ResourceName, hostAccountID),
|
||||
AccountStatus: providerAccountStatusFromLegacy(match.AccountStatus),
|
||||
LastProbeStatus: strings.TrimSpace(match.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
|
||||
}
|
||||
|
||||
type legacyBatchAccountProjection struct {
|
||||
KeyFingerprint string
|
||||
AccountStatus string
|
||||
ProbeStatus string
|
||||
AccountID string
|
||||
}
|
||||
|
||||
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 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 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user