mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-22 03:02:35 +00:00
349 lines
10 KiB
Go
349 lines
10 KiB
Go
package cloudcp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
cpDocker "github.com/rcourtman/pulse-go-rewrite/internal/cloudcp/docker"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/cloudcp/registry"
|
|
)
|
|
|
|
type ProofTenantAuditItem struct {
|
|
TenantID string
|
|
AccountID string
|
|
Email string
|
|
State registry.TenantState
|
|
CreatedAt time.Time
|
|
Age time.Duration
|
|
}
|
|
|
|
type ProofAccountAuditItem struct {
|
|
AccountID string
|
|
Kind registry.AccountKind
|
|
CreatedAt time.Time
|
|
Age time.Duration
|
|
}
|
|
|
|
type HostedEntitlementAuditItem struct {
|
|
EntitlementID string
|
|
TenantID string
|
|
Kind registry.HostedEntitlementKind
|
|
IssuedAt time.Time
|
|
}
|
|
|
|
type CloudAuditReport struct {
|
|
OK bool
|
|
Failures []string
|
|
Storage *StorageGuardrailReport
|
|
TenantCounts map[registry.TenantState]int
|
|
TenantTotal int
|
|
RegistryUnhealthyActive int
|
|
DockerManagedTotal int
|
|
DockerManagedRunning int
|
|
DockerManagedUnhealthy int
|
|
DockerUnavailable string
|
|
StaleProofTenants []ProofTenantAuditItem
|
|
StaleProofAccounts []ProofAccountAuditItem
|
|
OrphanPaidHostedEntitlements []HostedEntitlementAuditItem
|
|
ManagedRuntimeContainers []cpDocker.RuntimeContainerSummary
|
|
}
|
|
|
|
func AuditCloud(ctx context.Context, cfg *CPConfig) (*CloudAuditReport, error) {
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("control plane config is required")
|
|
}
|
|
report := &CloudAuditReport{
|
|
OK: true,
|
|
TenantCounts: make(map[registry.TenantState]int),
|
|
}
|
|
|
|
reg, err := registry.NewTenantRegistry(cfg.ControlPlaneDir())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open tenant registry: %w", err)
|
|
}
|
|
defer reg.Close()
|
|
|
|
tenants, err := reg.List()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list tenants: %w", err)
|
|
}
|
|
tenantIDs := make(map[string]struct{}, len(tenants))
|
|
report.TenantTotal = len(tenants)
|
|
for _, tenant := range tenants {
|
|
if tenant == nil {
|
|
continue
|
|
}
|
|
tenantIDs[strings.TrimSpace(tenant.ID)] = struct{}{}
|
|
report.TenantCounts[tenant.State]++
|
|
if tenant.State == registry.TenantStateActive && !tenant.HealthCheckOK {
|
|
report.RegistryUnhealthyActive++
|
|
}
|
|
}
|
|
if report.RegistryUnhealthyActive > 0 {
|
|
report.addFailure(fmt.Sprintf("%d active tenants are unhealthy in the registry", report.RegistryUnhealthyActive))
|
|
}
|
|
report.StaleProofTenants = findStaleProofTenants(tenants, cfg.ProofTenantMatchers, cfg.ProofTenantMaxAge, time.Now().UTC())
|
|
if len(report.StaleProofTenants) > 0 {
|
|
report.addFailure(fmt.Sprintf("%d proof/canary tenants are older than %s", len(report.StaleProofTenants), cfg.ProofTenantMaxAge))
|
|
}
|
|
|
|
entitlements, err := reg.ListHostedEntitlements()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list hosted entitlements: %w", err)
|
|
}
|
|
report.OrphanPaidHostedEntitlements = findOrphanPaidHostedEntitlements(entitlements, tenantIDs)
|
|
if len(report.OrphanPaidHostedEntitlements) > 0 {
|
|
report.addFailure(fmt.Sprintf("%d paid hosted entitlements reference missing tenants", len(report.OrphanPaidHostedEntitlements)))
|
|
}
|
|
|
|
accounts, err := reg.ListAccounts()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list accounts: %w", err)
|
|
}
|
|
stripeAccounts, err := reg.ListStripeAccounts()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list stripe accounts: %w", err)
|
|
}
|
|
report.StaleProofAccounts = findStaleProofAccounts(accounts, stripeAccounts, cfg.ProofTenantMatchers, cfg.ProofTenantMaxAge, time.Now().UTC())
|
|
if len(report.StaleProofAccounts) > 0 {
|
|
report.addFailure(fmt.Sprintf("%d proof/canary accounts are older than %s", len(report.StaleProofAccounts), cfg.ProofTenantMaxAge))
|
|
}
|
|
|
|
dockerMgr, err := cpDocker.NewManager(cpDocker.ManagerConfig{
|
|
Image: cfg.PulseImage,
|
|
Network: cfg.DockerNetwork,
|
|
BaseDomain: baseDomainFromURL(cfg.BaseURL),
|
|
TrialActivationPublicKey: cfg.TrialActivationPublicKey,
|
|
TrustedProxyCIDRs: cfg.TrustedProxyCIDRs,
|
|
MemoryLimit: cfg.TenantMemoryLimit,
|
|
CPUShares: cfg.TenantCPUShares,
|
|
TenantLogMaxSize: cfg.TenantLogMaxSize,
|
|
TenantLogMaxFile: cfg.TenantLogMaxFile,
|
|
})
|
|
if err != nil {
|
|
report.DockerUnavailable = err.Error()
|
|
report.addFailure("docker manager unavailable: " + err.Error())
|
|
} else {
|
|
defer dockerMgr.Close()
|
|
containers, listErr := dockerMgr.ListManagedRuntimeContainers(ctx)
|
|
if listErr != nil {
|
|
report.DockerUnavailable = listErr.Error()
|
|
report.addFailure("managed tenant container audit failed: " + listErr.Error())
|
|
} else {
|
|
report.ManagedRuntimeContainers = containers
|
|
report.DockerManagedTotal = len(containers)
|
|
for _, container := range containers {
|
|
if container.State == "running" {
|
|
report.DockerManagedRunning++
|
|
}
|
|
if runtimeContainerUnhealthy(container) {
|
|
report.DockerManagedUnhealthy++
|
|
}
|
|
}
|
|
if report.DockerManagedUnhealthy > 0 {
|
|
report.addFailure(fmt.Sprintf("%d managed tenant containers are not healthy", report.DockerManagedUnhealthy))
|
|
}
|
|
}
|
|
}
|
|
|
|
storage, err := CheckStorageGuardrails(ctx, cfg, dockerMgr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
report.Storage = storage
|
|
if !storage.OK {
|
|
report.addFailure("storage guardrails failed: " + strings.Join(storage.Failures, "; "))
|
|
}
|
|
return report, nil
|
|
}
|
|
|
|
func (r *CloudAuditReport) addFailure(failure string) {
|
|
if r == nil {
|
|
return
|
|
}
|
|
failure = strings.TrimSpace(failure)
|
|
if failure == "" {
|
|
return
|
|
}
|
|
r.OK = false
|
|
r.Failures = append(r.Failures, failure)
|
|
}
|
|
|
|
func runtimeContainerUnhealthy(container cpDocker.RuntimeContainerSummary) bool {
|
|
if container.State != "running" {
|
|
return true
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(container.HealthStatus)) {
|
|
case "", "none", "healthy":
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func findStaleProofTenants(tenants []*registry.Tenant, matchers []string, maxAge time.Duration, now time.Time) []ProofTenantAuditItem {
|
|
if maxAge <= 0 {
|
|
return nil
|
|
}
|
|
cutoff := now.Add(-maxAge)
|
|
items := make([]ProofTenantAuditItem, 0)
|
|
for _, tenant := range tenants {
|
|
if tenant == nil || tenant.CreatedAt.IsZero() || tenant.CreatedAt.After(cutoff) {
|
|
continue
|
|
}
|
|
if !matchesProofTenant(tenant, matchers) {
|
|
continue
|
|
}
|
|
createdAt := tenant.CreatedAt.UTC()
|
|
items = append(items, ProofTenantAuditItem{
|
|
TenantID: strings.TrimSpace(tenant.ID),
|
|
AccountID: strings.TrimSpace(tenant.AccountID),
|
|
Email: strings.TrimSpace(tenant.Email),
|
|
State: tenant.State,
|
|
CreatedAt: createdAt,
|
|
Age: now.Sub(createdAt),
|
|
})
|
|
}
|
|
sort.Slice(items, func(i, j int) bool {
|
|
if items[i].CreatedAt.Equal(items[j].CreatedAt) {
|
|
return items[i].TenantID < items[j].TenantID
|
|
}
|
|
return items[i].CreatedAt.Before(items[j].CreatedAt)
|
|
})
|
|
return items
|
|
}
|
|
|
|
func findOrphanPaidHostedEntitlements(entitlements []*registry.HostedEntitlement, tenantIDs map[string]struct{}) []HostedEntitlementAuditItem {
|
|
items := make([]HostedEntitlementAuditItem, 0)
|
|
for _, entitlement := range entitlements {
|
|
if entitlement == nil || entitlement.Kind != registry.HostedEntitlementKindPaid {
|
|
continue
|
|
}
|
|
tenantID := strings.TrimSpace(entitlement.TenantID)
|
|
if tenantID == "" {
|
|
continue
|
|
}
|
|
if _, ok := tenantIDs[tenantID]; ok {
|
|
continue
|
|
}
|
|
items = append(items, HostedEntitlementAuditItem{
|
|
EntitlementID: strings.TrimSpace(entitlement.ID),
|
|
TenantID: tenantID,
|
|
Kind: entitlement.Kind,
|
|
IssuedAt: entitlement.IssuedAt.UTC(),
|
|
})
|
|
}
|
|
sort.Slice(items, func(i, j int) bool {
|
|
if items[i].IssuedAt.Equal(items[j].IssuedAt) {
|
|
return items[i].EntitlementID < items[j].EntitlementID
|
|
}
|
|
return items[i].IssuedAt.Before(items[j].IssuedAt)
|
|
})
|
|
return items
|
|
}
|
|
|
|
func findStaleProofAccounts(accounts []*registry.Account, stripeAccounts []*registry.StripeAccount, matchers []string, maxAge time.Duration, now time.Time) []ProofAccountAuditItem {
|
|
if maxAge <= 0 {
|
|
return nil
|
|
}
|
|
stripeByAccount := make(map[string][]*registry.StripeAccount, len(stripeAccounts))
|
|
for _, stripeAccount := range stripeAccounts {
|
|
if stripeAccount == nil {
|
|
continue
|
|
}
|
|
accountID := strings.TrimSpace(stripeAccount.AccountID)
|
|
if accountID == "" {
|
|
continue
|
|
}
|
|
stripeByAccount[accountID] = append(stripeByAccount[accountID], stripeAccount)
|
|
}
|
|
|
|
cutoff := now.Add(-maxAge)
|
|
items := make([]ProofAccountAuditItem, 0)
|
|
for _, account := range accounts {
|
|
if account == nil || account.CreatedAt.IsZero() || account.CreatedAt.After(cutoff) {
|
|
continue
|
|
}
|
|
if !matchesProofAccount(account, stripeByAccount[account.ID], matchers) {
|
|
continue
|
|
}
|
|
createdAt := account.CreatedAt.UTC()
|
|
items = append(items, ProofAccountAuditItem{
|
|
AccountID: strings.TrimSpace(account.ID),
|
|
Kind: account.Kind,
|
|
CreatedAt: createdAt,
|
|
Age: now.Sub(createdAt),
|
|
})
|
|
}
|
|
sort.Slice(items, func(i, j int) bool {
|
|
if items[i].CreatedAt.Equal(items[j].CreatedAt) {
|
|
return items[i].AccountID < items[j].AccountID
|
|
}
|
|
return items[i].CreatedAt.Before(items[j].CreatedAt)
|
|
})
|
|
return items
|
|
}
|
|
|
|
func matchesProofTenant(tenant *registry.Tenant, matchers []string) bool {
|
|
if tenant == nil {
|
|
return false
|
|
}
|
|
parts := []string{
|
|
tenant.ID,
|
|
tenant.AccountID,
|
|
tenant.Email,
|
|
tenant.DisplayName,
|
|
tenant.StripeCustomerID,
|
|
tenant.StripeSubscriptionID,
|
|
tenant.StripePriceID,
|
|
tenant.PlanVersion,
|
|
}
|
|
haystack := strings.ToLower(strings.Join(parts, " "))
|
|
for _, matcher := range matchers {
|
|
matcher = strings.ToLower(strings.TrimSpace(matcher))
|
|
if matcher == "" {
|
|
continue
|
|
}
|
|
if strings.Contains(haystack, matcher) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func matchesProofAccount(account *registry.Account, stripeAccounts []*registry.StripeAccount, matchers []string) bool {
|
|
if account == nil {
|
|
return false
|
|
}
|
|
parts := []string{
|
|
account.ID,
|
|
string(account.Kind),
|
|
account.DisplayName,
|
|
}
|
|
for _, stripeAccount := range stripeAccounts {
|
|
if stripeAccount == nil {
|
|
continue
|
|
}
|
|
parts = append(parts,
|
|
stripeAccount.StripeCustomerID,
|
|
stripeAccount.StripeSubscriptionID,
|
|
stripeAccount.StripeSubItemWorkspacesID,
|
|
stripeAccount.PlanVersion,
|
|
stripeAccount.SubscriptionState,
|
|
)
|
|
}
|
|
haystack := strings.ToLower(strings.Join(parts, " "))
|
|
for _, matcher := range matchers {
|
|
matcher = strings.ToLower(strings.TrimSpace(matcher))
|
|
if matcher == "" {
|
|
continue
|
|
}
|
|
if strings.Contains(haystack, matcher) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|