From 4043c466f67ea8bbb01776ad948cb5fb32d454a0 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 7 Jul 2026 20:49:55 +0100 Subject: [PATCH] Extend MSP install proof for portal rollups --- .../provider_msp_install_proof.go | 17 +- .../provider_msp_install_proof_test.go | 53 +-- cmd/pulse-control-plane/provider_msp_proof.go | 337 +++++++++++++++--- .../provider_msp_proof_test.go | 20 +- .../v6/internal/subsystems/cloud-paid.md | 5 + .../subsystems/deployment-installability.md | 8 +- internal/cloudcp/portal/setup_facts.go | 47 ++- internal/cloudcp/portal/setup_facts_test.go | 24 ++ 8 files changed, 422 insertions(+), 89 deletions(-) diff --git a/cmd/pulse-control-plane/provider_msp_install_proof.go b/cmd/pulse-control-plane/provider_msp_install_proof.go index 08e37abe0..69a339554 100644 --- a/cmd/pulse-control-plane/provider_msp_install_proof.go +++ b/cmd/pulse-control-plane/provider_msp_install_proof.go @@ -65,6 +65,8 @@ type providerMSPInstallProofReport struct { SetupFactsTokenUseVisible bool AgentReportIngestVerified bool TokenRotationVerified bool + ReportScheduleVisible bool + ActiveAlertRollupVisible bool RotatedOutTokenRejectionVerified bool NonProofTenantCountPreserved bool InitialStatusTotalTenants int @@ -495,6 +497,8 @@ func populateProviderMSPInstallProofProof(report *providerMSPInstallProofReport, report.SetupFactsTokenUseVisible = proof.SetupFactsTokenUseVisible report.AgentReportIngestVerified = proof.AgentReportIngestVerified report.TokenRotationVerified = proof.TokenRotationVerified + report.ReportScheduleVisible = proof.ReportScheduleVisible + report.ActiveAlertRollupVisible = proof.ActiveAlertRollupVisible report.TenantIsolationVerified = proof.InstallTokenBoundaryOK report.DefaultRuntimeIsolationVerified = proof.AgentReportIngestVerified report.RotatedOutTokenRejectionVerified = providerMSPInstallProofAllRotatedOutTokensRejected(proof.Workspaces) @@ -596,6 +600,8 @@ func printProviderMSPInstallProofReport(report *providerMSPInstallProofReport) { fmt.Printf("setup_facts_token_use_visible=%t\n", report.SetupFactsTokenUseVisible) fmt.Printf("agent_report_ingest_verified=%t\n", report.AgentReportIngestVerified) fmt.Printf("token_rotation_verified=%t\n", report.TokenRotationVerified) + fmt.Printf("report_schedule_visible=%t\n", report.ReportScheduleVisible) + fmt.Printf("active_alert_rollup_visible=%t\n", report.ActiveAlertRollupVisible) fmt.Printf("rotated_out_token_rejection_verified=%t\n", report.RotatedOutTokenRejectionVerified) fmt.Printf("non_proof_tenant_count_preserved=%t\n", report.NonProofTenantCountPreserved) fmt.Printf("initial_status_total_tenants=%d\n", report.InitialStatusTotalTenants) @@ -605,7 +611,7 @@ func printProviderMSPInstallProofReport(report *providerMSPInstallProofReport) { fmt.Printf("final_status_unhealthy_tenants=%d\n", report.FinalStatusUnhealthyTenants) fmt.Printf("final_status_stuck_provisioning_tenants=%d\n", report.FinalStatusStuckProvisioningTenants) for _, workspace := range report.Workspaces { - fmt.Printf("workspace=%s display_name=%q state=%s plan_version=%s container_id=%s public_url=%s install_type=%s install_token_id=%s install_command_generated=%t agent_token_auth_verified=%t setup_facts_token_use_visible=%t agent_report_ingest_verified=%t agent_report_agent_id=%s agent_report_hostname=%s token_rotation_verified=%t rotated_install_token_id=%s old_install_token_rejected=%t rotated_agent_report_verified=%t handoff_exchange_verified=%t handoff_target_path=%s entitlement_lease_checked=%t entitlement_lease_verified=%t entitlement_white_label=%t entitlement_skipped_reason=%s\n", + fmt.Printf("workspace=%s display_name=%q state=%s plan_version=%s container_id=%s public_url=%s install_type=%s install_token_id=%s install_command_generated=%t agent_token_auth_verified=%t setup_facts_token_use_visible=%t agent_report_ingest_verified=%t agent_report_agent_id=%s agent_report_hostname=%s token_rotation_verified=%t rotated_install_token_id=%s old_install_token_rejected=%t rotated_agent_report_verified=%t handoff_exchange_verified=%t handoff_target_path=%s entitlement_lease_checked=%t entitlement_lease_verified=%t entitlement_white_label=%t entitlement_skipped_reason=%s report_schedule_created=%t report_schedule_id=%s report_schedule_visible=%t report_schedule_count=%d disabled_report_schedule_count=%d active_alert_persisted=%t active_alert_rollup_visible=%t critical_alert_count=%d warning_alert_count=%d\n", workspace.TenantID, workspace.DisplayName, workspace.State, @@ -630,6 +636,15 @@ func printProviderMSPInstallProofReport(report *providerMSPInstallProofReport) { workspace.EntitlementLeaseVerified, workspace.EntitlementWhiteLabel, workspace.EntitlementSkippedReason, + workspace.ReportScheduleCreated, + workspace.ReportScheduleID, + workspace.ReportScheduleVisible, + workspace.ReportScheduleCount, + workspace.DisabledReportScheduleCount, + workspace.ActiveAlertPersisted, + workspace.ActiveAlertRollupVisible, + workspace.CriticalAlertCount, + workspace.WarningAlertCount, ) } for _, failure := range report.Failures { diff --git a/cmd/pulse-control-plane/provider_msp_install_proof_test.go b/cmd/pulse-control-plane/provider_msp_install_proof_test.go index f33342ddc..9e1fc07b7 100644 --- a/cmd/pulse-control-plane/provider_msp_install_proof_test.go +++ b/cmd/pulse-control-plane/provider_msp_install_proof_test.go @@ -159,7 +159,7 @@ func TestProviderMSPInstallProofRunsFreshInstallSequence(t *testing.T) { if report.WorkspaceCount != 2 || len(report.Workspaces) != 2 { t.Fatalf("workspace proof count = %d len=%d", report.WorkspaceCount, len(report.Workspaces)) } - if !report.TenantIsolationVerified || !report.DefaultRuntimeIsolationVerified || !report.TokenRotationVerified || !report.RotatedOutTokenRejectionVerified { + if !report.TenantIsolationVerified || !report.DefaultRuntimeIsolationVerified || !report.TokenRotationVerified || !report.RotatedOutTokenRejectionVerified || !report.ReportScheduleVisible || !report.ActiveAlertRollupVisible { t.Fatalf("isolation/token proof missing: %#v", report) } if !report.NonProofTenantCountPreserved { @@ -336,6 +336,8 @@ func healthyProviderMSPInstallProofProofReport() *providerMSPProofReport { SetupFactsTokenUseVisible: true, AgentReportIngestVerified: true, TokenRotationVerified: true, + ReportScheduleVisible: true, + ActiveAlertRollupVisible: true, Workspaces: []providerMSPProofWorkspace{ healthyProviderMSPInstallProofWorkspace("ws-proof-01", "Provider MSP Proof 01"), healthyProviderMSPInstallProofWorkspace("ws-proof-02", "Provider MSP Proof 02"), @@ -345,26 +347,35 @@ func healthyProviderMSPInstallProofProofReport() *providerMSPProofReport { func healthyProviderMSPInstallProofWorkspace(tenantID, displayName string) providerMSPProofWorkspace { return providerMSPProofWorkspace{ - TenantID: tenantID, - DisplayName: displayName, - State: "active", - PlanVersion: "msp_growth", - ContainerID: "ctr-" + tenantID, - PublicURL: "https://" + tenantID + ".msp.example.com", - InstallType: "pve", - InstallTokenID: "tok-" + tenantID, - InstallCommandGenerated: true, - AgentTokenAuthVerified: true, - SetupFactsTokenUseVisible: true, - AgentReportIngestVerified: true, - AgentReportAgentID: "agent-" + tenantID, - AgentReportHostname: "pve1", - TokenRotationVerified: true, - RotatedInstallTokenID: "tok-rotated-" + tenantID, - OldInstallTokenRejected: true, - RotatedAgentReportVerified: true, - HandoffExchangeVerified: true, - HandoffTargetPath: "/settings/infrastructure?add=linux-host", + TenantID: tenantID, + DisplayName: displayName, + State: "active", + PlanVersion: "msp_growth", + ContainerID: "ctr-" + tenantID, + PublicURL: "https://" + tenantID + ".msp.example.com", + InstallType: "pve", + InstallTokenID: "tok-" + tenantID, + InstallCommandGenerated: true, + AgentTokenAuthVerified: true, + SetupFactsTokenUseVisible: true, + AgentReportIngestVerified: true, + AgentReportAgentID: "agent-" + tenantID, + AgentReportHostname: "pve1", + TokenRotationVerified: true, + RotatedInstallTokenID: "tok-rotated-" + tenantID, + OldInstallTokenRejected: true, + RotatedAgentReportVerified: true, + HandoffExchangeVerified: true, + HandoffTargetPath: "/settings/infrastructure?add=linux-host", + ReportScheduleCreated: true, + ReportScheduleID: "sched-" + tenantID, + ReportScheduleVisible: true, + ReportScheduleCount: 1, + DisabledReportScheduleCount: 0, + ActiveAlertPersisted: true, + ActiveAlertRollupVisible: true, + CriticalAlertCount: 1, + WarningAlertCount: 1, } } diff --git a/cmd/pulse-control-plane/provider_msp_proof.go b/cmd/pulse-control-plane/provider_msp_proof.go index f8c987ae2..14a6bfedb 100644 --- a/cmd/pulse-control-plane/provider_msp_proof.go +++ b/cmd/pulse-control-plane/provider_msp_proof.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/rcourtman/pulse-go-rewrite/internal/alerts" "github.com/rcourtman/pulse-go-rewrite/internal/api" "github.com/rcourtman/pulse-go-rewrite/internal/cloudcp" "github.com/rcourtman/pulse-go-rewrite/internal/cloudcp/account" @@ -55,32 +56,41 @@ type providerMSPProofRuntime struct { } type providerMSPProofWorkspace struct { - TenantID string - DisplayName string - State string - PlanVersion string - ContainerID string - PublicURL string - InstallType string - InstallToken string - InstallTokenID string - InstallCommandGenerated bool - AgentTokenAuthVerified bool - SetupFactsTokenUseVisible bool - AgentReportIngestVerified bool - AgentReportAgentID string - AgentReportHostname string - TokenRotationVerified bool - RotatedInstallToken string - RotatedInstallTokenID string - OldInstallTokenRejected bool - RotatedAgentReportVerified bool - HandoffExchangeVerified bool - HandoffTargetPath string - EntitlementLeaseChecked bool - EntitlementLeaseVerified bool - EntitlementWhiteLabel bool - EntitlementSkippedReason string + TenantID string + DisplayName string + State string + PlanVersion string + ContainerID string + PublicURL string + InstallType string + InstallToken string + InstallTokenID string + InstallCommandGenerated bool + AgentTokenAuthVerified bool + SetupFactsTokenUseVisible bool + AgentReportIngestVerified bool + AgentReportAgentID string + AgentReportHostname string + TokenRotationVerified bool + RotatedInstallToken string + RotatedInstallTokenID string + OldInstallTokenRejected bool + RotatedAgentReportVerified bool + HandoffExchangeVerified bool + HandoffTargetPath string + EntitlementLeaseChecked bool + EntitlementLeaseVerified bool + EntitlementWhiteLabel bool + EntitlementSkippedReason string + ReportScheduleCreated bool + ReportScheduleID string + ReportScheduleVisible bool + ReportScheduleCount int + DisabledReportScheduleCount int + ActiveAlertPersisted bool + ActiveAlertRollupVisible bool + CriticalAlertCount int + WarningAlertCount int } type providerMSPProofReport struct { @@ -102,6 +112,8 @@ type providerMSPProofReport struct { SetupFactsTokenUseVisible bool AgentReportIngestVerified bool TokenRotationVerified bool + ReportScheduleVisible bool + ActiveAlertRollupVisible bool Cleanup bool } @@ -268,6 +280,8 @@ func (rt *providerMSPProofRuntime) runProviderMSPProof(ctx context.Context, opts SetupFactsTokenUseVisible: true, AgentReportIngestVerified: true, TokenRotationVerified: true, + ReportScheduleVisible: true, + ActiveAlertRollupVisible: true, Cleanup: opts.Cleanup, } @@ -299,6 +313,12 @@ func (rt *providerMSPProofRuntime) runProviderMSPProof(ctx context.Context, opts if !workspace.TokenRotationVerified { report.TokenRotationVerified = false } + if !workspace.ReportScheduleVisible { + report.ReportScheduleVisible = false + } + if !workspace.ActiveAlertRollupVisible { + report.ActiveAlertRollupVisible = false + } report.Workspaces = append(report.Workspaces, workspace) } @@ -461,36 +481,236 @@ func (rt *providerMSPProofRuntime) proveProviderMSPWorkspace(ctx context.Context return providerMSPProofWorkspace{}, err } + portalRollup, err := rt.verifyProviderMSPProofPortalRollup(ctx, tenant, tenantDataDir, agentReport) + if err != nil { + return providerMSPProofWorkspace{}, err + } + return providerMSPProofWorkspace{ - TenantID: tenant.ID, - DisplayName: tenant.DisplayName, - State: string(tenant.State), - PlanVersion: tenant.PlanVersion, - ContainerID: tenant.ContainerID, - PublicURL: publicURL, - InstallType: install.InstallType, - InstallToken: install.Token, - InstallTokenID: install.TokenID, - InstallCommandGenerated: true, - AgentTokenAuthVerified: tokenAuthVerified, - SetupFactsTokenUseVisible: setupFactsVisible, - AgentReportIngestVerified: agentReport.Verified, - AgentReportAgentID: agentReport.AgentID, - AgentReportHostname: agentReport.Hostname, - TokenRotationVerified: rotation.Verified, - RotatedInstallToken: rotation.RotatedToken, - RotatedInstallTokenID: rotation.RotatedTokenID, - OldInstallTokenRejected: rotation.OldTokenRejected, - RotatedAgentReportVerified: rotation.RotatedAgentReportVerified, - HandoffExchangeVerified: exchangedTargetPath == targetPath, - HandoffTargetPath: exchangedTargetPath, - EntitlementLeaseChecked: entitlement.Checked, - EntitlementLeaseVerified: entitlement.Verified, - EntitlementWhiteLabel: entitlement.WhiteLabel, - EntitlementSkippedReason: entitlement.SkippedReason, + TenantID: tenant.ID, + DisplayName: tenant.DisplayName, + State: string(tenant.State), + PlanVersion: tenant.PlanVersion, + ContainerID: tenant.ContainerID, + PublicURL: publicURL, + InstallType: install.InstallType, + InstallToken: install.Token, + InstallTokenID: install.TokenID, + InstallCommandGenerated: true, + AgentTokenAuthVerified: tokenAuthVerified, + SetupFactsTokenUseVisible: setupFactsVisible, + AgentReportIngestVerified: agentReport.Verified, + AgentReportAgentID: agentReport.AgentID, + AgentReportHostname: agentReport.Hostname, + TokenRotationVerified: rotation.Verified, + RotatedInstallToken: rotation.RotatedToken, + RotatedInstallTokenID: rotation.RotatedTokenID, + OldInstallTokenRejected: rotation.OldTokenRejected, + RotatedAgentReportVerified: rotation.RotatedAgentReportVerified, + HandoffExchangeVerified: exchangedTargetPath == targetPath, + HandoffTargetPath: exchangedTargetPath, + EntitlementLeaseChecked: entitlement.Checked, + EntitlementLeaseVerified: entitlement.Verified, + EntitlementWhiteLabel: entitlement.WhiteLabel, + EntitlementSkippedReason: entitlement.SkippedReason, + ReportScheduleCreated: portalRollup.ReportScheduleCreated, + ReportScheduleID: portalRollup.ReportScheduleID, + ReportScheduleVisible: portalRollup.ReportScheduleVisible, + ReportScheduleCount: portalRollup.ReportScheduleCount, + DisabledReportScheduleCount: portalRollup.DisabledReportScheduleCount, + ActiveAlertPersisted: portalRollup.ActiveAlertPersisted, + ActiveAlertRollupVisible: portalRollup.ActiveAlertRollupVisible, + CriticalAlertCount: portalRollup.CriticalAlertCount, + WarningAlertCount: portalRollup.WarningAlertCount, }, nil } +type providerMSPProofPortalRollup struct { + ReportScheduleCreated bool + ReportScheduleID string + ReportScheduleVisible bool + ReportScheduleCount int + DisabledReportScheduleCount int + ActiveAlertPersisted bool + ActiveAlertRollupVisible bool + CriticalAlertCount int + WarningAlertCount int +} + +func (rt *providerMSPProofRuntime) verifyProviderMSPProofPortalRollup(ctx context.Context, tenant *registry.Tenant, tenantDataDir string, agentReport providerMSPProofAgentReportIngest) (providerMSPProofPortalRollup, error) { + result := providerMSPProofPortalRollup{} + if tenant == nil { + return result, fmt.Errorf("tenant is required") + } + tenantID := strings.TrimSpace(tenant.ID) + if tenantID == "" { + return result, fmt.Errorf("tenant id is required") + } + if strings.TrimSpace(agentReport.AgentID) == "" { + return result, fmt.Errorf("tenant %s agent report id is required before portal rollup proof", tenantID) + } + + scheduleID, err := rt.createProviderMSPProofReportSchedule(ctx, tenant, tenantDataDir, agentReport.AgentID, agentReport.Hostname) + if err != nil { + return result, err + } + result.ReportScheduleCreated = true + result.ReportScheduleID = scheduleID + + orgDir := filepath.Join(tenantDataDir, "orgs", tenantID) + if err := writeProviderMSPProofActiveAlerts(orgDir, agentReport.AgentID, agentReport.Hostname); err != nil { + return result, fmt.Errorf("write provider MSP proof active alerts for tenant %s: %w", tenantID, err) + } + result.ActiveAlertPersisted = true + + facts := portal.NewTenantDirWorkspaceSetupFactReader(rt.cfg.TenantsDir()).FactsForWorkspace(tenantID) + result.ReportScheduleCount = providerMSPProofFactInt(facts.ReportScheduleCount) + result.DisabledReportScheduleCount = providerMSPProofFactInt(facts.DisabledReportScheduleCount) + result.CriticalAlertCount = providerMSPProofFactInt(facts.ActiveCriticalAlertCount) + result.WarningAlertCount = providerMSPProofFactInt(facts.ActiveWarningAlertCount) + result.ReportScheduleVisible = result.ReportScheduleCount == 1 && result.DisabledReportScheduleCount == 0 + result.ActiveAlertRollupVisible = result.CriticalAlertCount == 1 && result.WarningAlertCount == 1 && facts.ActiveAlertsUpdatedAt != nil + if !result.ReportScheduleVisible { + return result, fmt.Errorf("portal setup facts for tenant %s report schedules = enabled:%d disabled:%d, want enabled:1 disabled:0", tenantID, result.ReportScheduleCount, result.DisabledReportScheduleCount) + } + if !result.ActiveAlertRollupVisible { + return result, fmt.Errorf("portal setup facts for tenant %s active alerts = critical:%d warning:%d updated:%t, want critical:1 warning:1 updated:true", tenantID, result.CriticalAlertCount, result.WarningAlertCount, facts.ActiveAlertsUpdatedAt != nil) + } + return result, nil +} + +func (rt *providerMSPProofRuntime) createProviderMSPProofReportSchedule(ctx context.Context, tenant *registry.Tenant, tenantDataDir, resourceID, resourceName string) (string, error) { + if tenant == nil { + return "", fmt.Errorf("tenant is required") + } + tenantID := strings.TrimSpace(tenant.ID) + if tenantID == "" { + return "", fmt.Errorf("tenant id is required") + } + if strings.TrimSpace(resourceName) == "" { + resourceName = strings.TrimSpace(resourceID) + } + tenantCfg := &runtimeconfig.Config{ + DataPath: tenantDataDir, + ConfigPath: tenantDataDir, + PublicURL: rt.providerMSPProofTenantPublicURL(tenantID), + } + tenantPersistence := runtimeconfig.NewMultiTenantPersistence(tenantDataDir) + if !tenantPersistence.OrgExists(tenantID) { + return "", fmt.Errorf("tenant %s organization metadata is missing before report schedule proof", tenantID) + } + tenantMonitor := monitoring.NewMultiTenantMonitor(tenantCfg, tenantPersistence, nil) + defer tenantMonitor.Stop() + + handler := api.NewReportingHandlers(tenantMonitor, nil) + payload := runtimeconfig.ReportSchedule{ + Name: "Provider MSP proof monthly report", + Enabled: true, + Cadence: runtimeconfig.ReportScheduleCadence{ + Type: runtimeconfig.ReportScheduleCadenceMonthly, + DayOfMonth: 1, + Time: "09:00", + Timezone: "UTC", + }, + Scope: runtimeconfig.ReportScheduleScope{ + Resources: []runtimeconfig.ReportScheduleResource{ + { + ResourceType: "agent", + ResourceID: strings.TrimSpace(resourceID), + Name: strings.TrimSpace(resourceName), + }, + }, + }, + Format: runtimeconfig.ReportScheduleFormatPDF, + Delivery: runtimeconfig.ReportScheduleDelivery{ + Method: runtimeconfig.ReportScheduleDeliveryDisk, + Attach: true, + SaveToDisk: true, + }, + } + body, err := json.Marshal(payload) + if err != nil { + return "", fmt.Errorf("marshal provider MSP proof report schedule: %w", err) + } + req := httptest.NewRequest(http.MethodPost, "/api/reports/schedules", bytes.NewReader(body)) + req = req.WithContext(context.WithValue(ctx, api.OrgIDContextKey, tenantID)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + handler.HandleCreateReportSchedule(rec, req) + if rec.Code != http.StatusCreated { + return "", fmt.Errorf("tenant %s report schedule create status=%d body=%s", tenantID, rec.Code, strings.TrimSpace(rec.Body.String())) + } + + var created runtimeconfig.ReportSchedule + if err := json.Unmarshal(rec.Body.Bytes(), &created); err != nil { + return "", fmt.Errorf("decode provider MSP proof report schedule: %w", err) + } + if strings.TrimSpace(created.ID) == "" { + return "", fmt.Errorf("tenant %s report schedule create response missing id", tenantID) + } + return created.ID, nil +} + +func writeProviderMSPProofActiveAlerts(orgDir, resourceID, resourceName string) error { + if strings.TrimSpace(orgDir) == "" { + return fmt.Errorf("org dir is required") + } + if strings.TrimSpace(resourceName) == "" { + resourceName = strings.TrimSpace(resourceID) + } + now := time.Now().UTC() + active := []alerts.Alert{ + { + ID: "provider-msp-proof-critical", + Type: "cpu", + Level: alerts.AlertLevelCritical, + ResourceID: strings.TrimSpace(resourceID), + ResourceName: strings.TrimSpace(resourceName), + Node: strings.TrimSpace(resourceName), + Instance: "provider-msp-proof", + Message: "Provider MSP proof critical alert", + Value: 96, + Threshold: 80, + StartTime: now, + LastSeen: now, + }, + { + ID: "provider-msp-proof-warning", + Type: "memory", + Level: alerts.AlertLevelWarning, + ResourceID: strings.TrimSpace(resourceID), + ResourceName: strings.TrimSpace(resourceName), + Node: strings.TrimSpace(resourceName), + Instance: "provider-msp-proof", + Message: "Provider MSP proof warning alert", + Value: 88, + Threshold: 85, + StartTime: now, + LastSeen: now, + }, + } + data, err := json.Marshal(active) + if err != nil { + return fmt.Errorf("marshal active alerts: %w", err) + } + alertsDir := filepath.Join(orgDir, "alerts") + if err := os.MkdirAll(alertsDir, 0o700); err != nil { + return fmt.Errorf("create active alerts dir: %w", err) + } + path := filepath.Join(alertsDir, "active-alerts.json") + if err := os.WriteFile(path, data, 0o600); err != nil { + return fmt.Errorf("write active alerts file: %w", err) + } + return nil +} + +func providerMSPProofFactInt(value *int) int { + if value == nil { + return 0 + } + return *value +} + type providerMSPProofEntitlementLease struct { Checked bool Verified bool @@ -1101,9 +1321,11 @@ func printProviderMSPProofReport(report *providerMSPProofReport) { fmt.Printf("setup_facts_token_use_visible=%t\n", report.SetupFactsTokenUseVisible) fmt.Printf("agent_report_ingest_verified=%t\n", report.AgentReportIngestVerified) fmt.Printf("token_rotation_verified=%t\n", report.TokenRotationVerified) + fmt.Printf("report_schedule_visible=%t\n", report.ReportScheduleVisible) + fmt.Printf("active_alert_rollup_visible=%t\n", report.ActiveAlertRollupVisible) fmt.Printf("cleanup=%t\n", report.Cleanup) for _, workspace := range report.Workspaces { - fmt.Printf("workspace=%s display_name=%q state=%s plan_version=%s container_id=%s public_url=%s install_type=%s install_token_id=%s install_command_generated=%t agent_token_auth_verified=%t setup_facts_token_use_visible=%t agent_report_ingest_verified=%t agent_report_agent_id=%s agent_report_hostname=%s token_rotation_verified=%t rotated_install_token_id=%s old_install_token_rejected=%t rotated_agent_report_verified=%t handoff_exchange_verified=%t handoff_target_path=%s entitlement_lease_checked=%t entitlement_lease_verified=%t entitlement_white_label=%t entitlement_skipped_reason=%s\n", + fmt.Printf("workspace=%s display_name=%q state=%s plan_version=%s container_id=%s public_url=%s install_type=%s install_token_id=%s install_command_generated=%t agent_token_auth_verified=%t setup_facts_token_use_visible=%t agent_report_ingest_verified=%t agent_report_agent_id=%s agent_report_hostname=%s token_rotation_verified=%t rotated_install_token_id=%s old_install_token_rejected=%t rotated_agent_report_verified=%t handoff_exchange_verified=%t handoff_target_path=%s entitlement_lease_checked=%t entitlement_lease_verified=%t entitlement_white_label=%t entitlement_skipped_reason=%s report_schedule_created=%t report_schedule_id=%s report_schedule_visible=%t report_schedule_count=%d disabled_report_schedule_count=%d active_alert_persisted=%t active_alert_rollup_visible=%t critical_alert_count=%d warning_alert_count=%d\n", workspace.TenantID, workspace.DisplayName, workspace.State, @@ -1128,6 +1350,15 @@ func printProviderMSPProofReport(report *providerMSPProofReport) { workspace.EntitlementLeaseVerified, workspace.EntitlementWhiteLabel, workspace.EntitlementSkippedReason, + workspace.ReportScheduleCreated, + workspace.ReportScheduleID, + workspace.ReportScheduleVisible, + workspace.ReportScheduleCount, + workspace.DisabledReportScheduleCount, + workspace.ActiveAlertPersisted, + workspace.ActiveAlertRollupVisible, + workspace.CriticalAlertCount, + workspace.WarningAlertCount, ) } } diff --git a/cmd/pulse-control-plane/provider_msp_proof_test.go b/cmd/pulse-control-plane/provider_msp_proof_test.go index 1230e220f..c1d34e6ba 100644 --- a/cmd/pulse-control-plane/provider_msp_proof_test.go +++ b/cmd/pulse-control-plane/provider_msp_proof_test.go @@ -141,6 +141,12 @@ func TestProviderMSPProofExercisesWorkspaceInstallHandoffAndIsolation(t *testing if !report.TokenRotationVerified { t.Fatal("token rotation was not verified") } + if !report.ReportScheduleVisible { + t.Fatal("report schedule portal fact was not verified") + } + if !report.ActiveAlertRollupVisible { + t.Fatal("active alert portal rollup was not verified") + } seenTenants := map[string]struct{}{} for _, workspace := range report.Workspaces { @@ -202,6 +208,18 @@ func TestProviderMSPProofExercisesWorkspaceInstallHandoffAndIsolation(t *testing if !workspace.EntitlementWhiteLabel { t.Fatalf("entitlement lease for %s is missing white_label", workspace.TenantID) } + if !workspace.ReportScheduleCreated || workspace.ReportScheduleID == "" || !workspace.ReportScheduleVisible { + t.Fatalf("report schedule portal proof incomplete for %s: %#v", workspace.TenantID, workspace) + } + if workspace.ReportScheduleCount != 1 || workspace.DisabledReportScheduleCount != 0 { + t.Fatalf("report schedule facts for %s = enabled:%d disabled:%d, want enabled:1 disabled:0", workspace.TenantID, workspace.ReportScheduleCount, workspace.DisabledReportScheduleCount) + } + if !workspace.ActiveAlertPersisted || !workspace.ActiveAlertRollupVisible { + t.Fatalf("active alert portal rollup proof incomplete for %s: %#v", workspace.TenantID, workspace) + } + if workspace.CriticalAlertCount != 1 || workspace.WarningAlertCount != 1 { + t.Fatalf("active alert facts for %s = critical:%d warning:%d, want critical:1 warning:1", workspace.TenantID, workspace.CriticalAlertCount, workspace.WarningAlertCount) + } } } @@ -254,7 +272,7 @@ func TestProviderMSPProofLoadsSignedLicenseFilePlan(t *testing.T) { if report.LicenseEmail != "provider@example.com" { t.Fatalf("LicenseEmail = %q, want provider@example.com", report.LicenseEmail) } - if !report.AgentReportIngestVerified || !report.InstallTokenBoundaryOK || !report.TokenRotationVerified || !report.HandoffExchangeVerified { + if !report.AgentReportIngestVerified || !report.InstallTokenBoundaryOK || !report.TokenRotationVerified || !report.HandoffExchangeVerified || !report.ReportScheduleVisible || !report.ActiveAlertRollupVisible { t.Fatalf("provider MSP proof did not complete core runtime checks: %#v", report) } } diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index c2dbc735a..9d5e56704 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -679,6 +679,11 @@ or other self-hosted uncapped continuity plans. age labels from read-only setup facts so providers can prioritize the workspace list, but it must not become an alert console or expose alert bodies, remediation state, acknowledgements, or cross-client alert streams. + Those setup facts must read report schedule counts from the client runtime's + org-scoped `report_schedules.json` store and active-alert counts from the + org-scoped `alerts/active-alerts.json` runtime file before falling back to a + legacy tenant-root active-alert file, because tenant monitors own + org-scoped runtime persistence. Pulse Account also owns the provider-facing setup progression for client workspaces: after workspace creation the portal should select the created workspace, reveal the setup job, and preserve workspace/target context in diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 1e00ddac9..8449899a3 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -165,9 +165,11 @@ TLS floor in the dynamic config. `pulse-control-plane provider-msp proof` must exercise the first-client onboarding path through workspace creation, client-bound install token generation, tenant-local unified-agent report ingest, tenant-bound install - token rotation, rotated-out token rejection, handoff exchange, and - duplicate-hostname isolation before provider-hosted MSP installability is - treated as proven. The proof is license-backed by default: `license_file` must be the + token rotation, rotated-out token rejection, handoff exchange, + tenant-runtime report schedule creation, portal-visible active-alert rollup + facts, and duplicate-hostname isolation before provider-hosted MSP + installability is treated as proven. The proof is license-backed by default: + `license_file` must be the resolved provider MSP plan source unless the operator explicitly opts into the local-development `--allow-env-plan` escape hatch. The same proof surface must also keep adversarial client-boundary probes in diff --git a/internal/cloudcp/portal/setup_facts.go b/internal/cloudcp/portal/setup_facts.go index 8e6af26bc..5b6189df2 100644 --- a/internal/cloudcp/portal/setup_facts.go +++ b/internal/cloudcp/portal/setup_facts.go @@ -66,7 +66,7 @@ func readWorkspaceSetupFacts(tenantID, tenantDataDir, orgDir string) WorkspaceSe facts.AgentCount, facts.AgentTokenCount, facts.UnusedAgentTokenCount, facts.LastAgentSeenAt = readAgentSetupFacts(tenantID, tenantDataDir, orgDir) facts.AlertRouteCount, facts.DisabledAlertRouteCount = readAlertRouteFacts(orgDir) - facts.ActiveCriticalAlertCount, facts.ActiveWarningAlertCount, facts.ActiveAlertsUpdatedAt = readActiveAlertFacts(tenantDataDir) + facts.ActiveCriticalAlertCount, facts.ActiveWarningAlertCount, facts.ActiveAlertsUpdatedAt = readActiveAlertFacts(tenantDataDir, orgDir) facts.ReportScheduleCount, facts.DisabledReportScheduleCount = readReportScheduleFacts(orgDir) return facts } @@ -219,26 +219,53 @@ func nonBlankCount(values []string) int { return count } -func readActiveAlertFacts(tenantDataDir string) (*int, *int, *time.Time) { - path, ok := safeConfigLeafPath(tenantDataDir, filepath.Join("alerts", "active-alerts.json")) +func readActiveAlertFacts(tenantDataDir, orgDir string) (*int, *int, *time.Time) { + for _, dir := range uniqueActiveAlertFactDirs(orgDir, tenantDataDir) { + critical, warning, updatedAt, ok := readActiveAlertFactsFromDir(dir) + if ok { + return critical, warning, updatedAt + } + } + return nil, nil, nil +} + +func uniqueActiveAlertFactDirs(values ...string) []string { + dirs := make([]string, 0, len(values)) + seen := map[string]struct{}{} + for _, value := range values { + value = strings.TrimSpace(value) + if value == "" { + continue + } + if _, exists := seen[value]; exists { + continue + } + seen[value] = struct{}{} + dirs = append(dirs, value) + } + return dirs +} + +func readActiveAlertFactsFromDir(configDir string) (*int, *int, *time.Time, bool) { + path, ok := safeConfigLeafPath(configDir, filepath.Join("alerts", "active-alerts.json")) if !ok { - return nil, nil, nil + return nil, nil, nil, false } data, err := os.ReadFile(path) if err != nil { if !errors.Is(err, os.ErrNotExist) { - log.Warn().Err(err).Str("tenant_data_dir", tenantDataDir).Msg("cloudcp.portal.setup_facts: read active alerts") + log.Warn().Err(err).Str("config_dir", configDir).Msg("cloudcp.portal.setup_facts: read active alerts") } - return nil, nil, nil + return nil, nil, nil, false } if len(strings.TrimSpace(string(data))) == 0 { - return nil, nil, nil + return nil, nil, nil, true } var active []alerts.Alert if err := json.Unmarshal(data, &active); err != nil { - log.Warn().Err(err).Str("tenant_data_dir", tenantDataDir).Msg("cloudcp.portal.setup_facts: parse active alerts") - return nil, nil, nil + log.Warn().Err(err).Str("config_dir", configDir).Msg("cloudcp.portal.setup_facts: parse active alerts") + return nil, nil, nil, true } criticalCount := 0 @@ -257,7 +284,7 @@ func readActiveAlertFacts(tenantDataDir string) (*int, *int, *time.Time) { ts := info.ModTime().UTC() updatedAt = &ts } - return intPtr(criticalCount), intPtr(warningCount), updatedAt + return intPtr(criticalCount), intPtr(warningCount), updatedAt, true } func readReportScheduleFacts(orgDir string) (*int, *int) { diff --git a/internal/cloudcp/portal/setup_facts_test.go b/internal/cloudcp/portal/setup_facts_test.go index f57d7dfa2..5b8a43efe 100644 --- a/internal/cloudcp/portal/setup_facts_test.go +++ b/internal/cloudcp/portal/setup_facts_test.go @@ -264,6 +264,30 @@ func TestTenantDirWorkspaceSetupFactReaderTreatsCorruptActiveAlertFileAsUnknown( } } +func TestTenantDirWorkspaceSetupFactReaderPrefersRuntimeOrgActiveAlerts(t *testing.T) { + tenantsDir := t.TempDir() + tenantDir := filepath.Join(tenantsDir, "ws_runtime_alerts") + orgDir := filepath.Join(tenantDir, "orgs", "ws_runtime_alerts") + if err := os.MkdirAll(orgDir, 0o755); err != nil { + t.Fatal(err) + } + writeSetupFactJSON(t, tenantDir, filepath.Join("alerts", "active-alerts.json"), []alerts.Alert{ + {ID: "legacy-warning", Level: alerts.AlertLevelWarning}, + }) + writeSetupFactJSON(t, orgDir, filepath.Join("alerts", "active-alerts.json"), []alerts.Alert{ + {ID: "runtime-critical", Level: alerts.AlertLevelCritical}, + {ID: "runtime-warning", Level: alerts.AlertLevelWarning}, + }) + + facts := NewTenantDirWorkspaceSetupFactReader(tenantsDir).FactsForWorkspace("ws_runtime_alerts") + if facts.ActiveCriticalAlertCount == nil || *facts.ActiveCriticalAlertCount != 1 { + t.Fatalf("ActiveCriticalAlertCount = %v, want 1", facts.ActiveCriticalAlertCount) + } + if facts.ActiveWarningAlertCount == nil || *facts.ActiveWarningAlertCount != 1 { + t.Fatalf("ActiveWarningAlertCount = %v, want 1", facts.ActiveWarningAlertCount) + } +} + func TestTenantDirWorkspaceSetupFactReaderCountsRuntimePersistedReportSchedules(t *testing.T) { tenantsDir := t.TempDir() orgDir := filepath.Join(tenantsDir, "ws_runtime", "orgs", "ws_runtime")