find($this->bulkRunId); if (! $run || $run->status !== 'pending') { return; } $bulkOperationService->start($run); $tenant = $run->tenant ?? Tenant::query()->find($run->tenant_id); if (! $tenant instanceof Tenant) { $this->appendRunFailure($run, [ 'type' => 'run', 'item_id' => (string) $this->backupSetId, 'reason_code' => 'backup_set_not_found', 'reason' => $bulkOperationService->sanitizeFailureReason('Tenant not found for run.'), ]); $bulkOperationService->fail($run, 'Tenant not found for run.'); return; } $backupSet = BackupSet::withTrashed() ->where('tenant_id', $tenant->getKey()) ->whereKey($this->backupSetId) ->first(); if (! $backupSet) { $this->appendRunFailure($run, [ 'type' => 'run', 'item_id' => (string) $this->backupSetId, 'reason_code' => 'backup_set_not_found', 'reason' => $bulkOperationService->sanitizeFailureReason('Backup set not found.'), ]); $bulkOperationService->fail($run, 'Backup set not found.'); return; } if ($backupSet->trashed()) { $this->appendRunFailure($run, [ 'type' => 'run', 'item_id' => (string) $backupSet->getKey(), 'reason_code' => 'backup_set_archived', 'reason' => $bulkOperationService->sanitizeFailureReason('Backup set is archived.'), ]); $bulkOperationService->fail($run, 'Backup set is archived.'); return; } $policyIds = $this->extractPolicyIds($run); if ($policyIds === []) { $bulkOperationService->complete($run); return; } if ((int) $run->total_items !== count($policyIds)) { $run->update(['total_items' => count($policyIds)]); } $existingBackupFailures = (array) Arr::get($backupSet->metadata ?? [], 'failures', []); $newBackupFailures = []; $didMutateBackupSet = false; $backupSetItemMutations = 0; $foundationMutations = 0; $foundationFailures = 0; /** @var array $activePolicyIds */ $activePolicyIds = BackupItem::query() ->where('backup_set_id', $backupSet->getKey()) ->whereIn('policy_id', $policyIds) ->pluck('policy_id') ->filter() ->map(fn (mixed $value): int => (int) $value) ->values() ->all(); $activePolicyIdSet = array_fill_keys($activePolicyIds, true); /** @var EloquentCollection $trashedItems */ $trashedItems = BackupItem::onlyTrashed() ->where('backup_set_id', $backupSet->getKey()) ->whereIn('policy_id', $policyIds) ->get() ->keyBy('policy_id'); /** @var EloquentCollection $policies */ $policies = Policy::query() ->where('tenant_id', $tenant->getKey()) ->whereIn('id', $policyIds) ->get() ->keyBy('id'); foreach ($policyIds as $policyId) { if (isset($activePolicyIdSet[$policyId])) { $bulkOperationService->recordSkippedWithReason( run: $run, itemId: (string) $policyId, reason: 'Already in backup set', reasonCode: 'already_in_backup_set', ); continue; } $trashed = $trashedItems->get($policyId); if ($trashed instanceof BackupItem) { $trashed->restore(); $activePolicyIdSet[$policyId] = true; $didMutateBackupSet = true; $backupSetItemMutations++; $bulkOperationService->recordSuccess($run); continue; } $policy = $policies->get($policyId); if (! $policy instanceof Policy) { $newBackupFailures[] = [ 'policy_id' => $policyId, 'reason' => $bulkOperationService->sanitizeFailureReason('Policy not found.'), 'status' => null, 'reason_code' => 'policy_not_found', ]; $didMutateBackupSet = true; $bulkOperationService->recordFailure( run: $run, itemId: (string) $policyId, reason: 'Policy not found.', reasonCode: 'policy_not_found', ); continue; } if ($policy->ignored_at) { $bulkOperationService->recordSkippedWithReason( run: $run, itemId: (string) $policyId, reason: 'Policy is ignored locally', reasonCode: 'policy_ignored', ); continue; } try { $captureResult = $captureOrchestrator->capture( policy: $policy, tenant: $tenant, includeAssignments: $this->includeAssignments, includeScopeTags: $this->includeScopeTags, createdBy: $run->user?->email ? Str::limit($run->user->email, 255, '') : null, metadata: [ 'source' => 'backup', 'backup_set_id' => $backupSet->getKey(), ], ); } catch (Throwable $throwable) { $reason = $bulkOperationService->sanitizeFailureReason($throwable->getMessage()); $newBackupFailures[] = [ 'policy_id' => $policyId, 'reason' => $reason, 'status' => null, 'reason_code' => 'unknown', ]; $didMutateBackupSet = true; $bulkOperationService->recordFailure( run: $run, itemId: (string) $policyId, reason: $reason, reasonCode: 'unknown', ); continue; } if (isset($captureResult['failure']) && is_array($captureResult['failure'])) { $failure = $captureResult['failure']; $status = isset($failure['status']) && is_numeric($failure['status']) ? (int) $failure['status'] : null; $reasonCode = $this->mapGraphFailureReasonCode($status); $reason = $bulkOperationService->sanitizeFailureReason((string) ($failure['reason'] ?? 'Graph capture failed.')); $newBackupFailures[] = [ 'policy_id' => $policyId, 'reason' => $reason, 'status' => $status, 'reason_code' => $reasonCode, ]; $didMutateBackupSet = true; $bulkOperationService->recordFailure( run: $run, itemId: (string) $policyId, reason: $reason, reasonCode: $reasonCode, ); continue; } $version = $captureResult['version'] ?? null; $captured = $captureResult['captured'] ?? null; if (! $version || ! is_array($captured)) { $newBackupFailures[] = [ 'policy_id' => $policyId, 'reason' => $bulkOperationService->sanitizeFailureReason('Capture result missing version payload.'), 'status' => null, 'reason_code' => 'unknown', ]; $didMutateBackupSet = true; $bulkOperationService->recordFailure( run: $run, itemId: (string) $policyId, reason: 'Capture result missing version payload.', reasonCode: 'unknown', ); continue; } $payload = $captured['payload'] ?? []; $metadata = is_array($captured['metadata'] ?? null) ? $captured['metadata'] : []; $assignments = is_array($captured['assignments'] ?? null) ? $captured['assignments'] : null; $scopeTags = is_array($captured['scope_tags'] ?? null) ? $captured['scope_tags'] : null; if (! is_array($payload)) { $payload = []; } $validation = $snapshotValidator->validate($payload); $warnings = $validation['warnings'] ?? []; $odataWarning = BackupItem::odataTypeWarning($payload, $policy->policy_type, $policy->platform); if ($odataWarning) { $warnings[] = $odataWarning; } if (! empty($warnings)) { $existingWarnings = is_array($metadata['warnings'] ?? null) ? $metadata['warnings'] : []; $metadata['warnings'] = array_values(array_unique(array_merge($existingWarnings, $warnings))); } if (is_array($scopeTags)) { $metadata['scope_tag_ids'] = $scopeTags['ids'] ?? null; $metadata['scope_tag_names'] = $scopeTags['names'] ?? null; } try { BackupItem::create([ 'tenant_id' => $tenant->getKey(), 'backup_set_id' => $backupSet->getKey(), 'policy_id' => $policy->getKey(), 'policy_version_id' => $version->getKey(), 'policy_identifier' => $policy->external_id, 'policy_type' => $policy->policy_type, 'platform' => $policy->platform, 'payload' => $payload, 'metadata' => $metadata, 'assignments' => $assignments, ]); } catch (QueryException $exception) { if ((string) $exception->getCode() === '23505') { $bulkOperationService->recordSkippedWithReason( run: $run, itemId: (string) $policyId, reason: 'Already in backup set', reasonCode: 'already_in_backup_set', ); continue; } throw $exception; } $activePolicyIdSet[$policyId] = true; $didMutateBackupSet = true; $backupSetItemMutations++; $bulkOperationService->recordSuccess($run); } if ($this->includeFoundations) { [$foundationOutcome, $foundationFailureEntries] = $this->captureFoundations( bulkOperationService: $bulkOperationService, foundationSnapshots: $foundationSnapshots, tenant: $tenant, backupSet: $backupSet, ); if (($foundationOutcome['created'] ?? 0) > 0 || ($foundationOutcome['restored'] ?? 0) > 0) { $didMutateBackupSet = true; $foundationMutations = (int) $foundationOutcome['created'] + (int) $foundationOutcome['restored']; } if ($foundationFailureEntries !== []) { $didMutateBackupSet = true; $foundationFailures = count($foundationFailureEntries); $newBackupFailures = array_merge($newBackupFailures, $foundationFailureEntries); foreach ($foundationFailureEntries as $foundationFailure) { $this->appendRunFailure($run, [ 'type' => 'foundation', 'item_id' => (string) ($foundationFailure['foundation_type'] ?? 'foundation'), 'reason_code' => (string) ($foundationFailure['reason_code'] ?? 'unknown'), 'reason' => (string) ($foundationFailure['reason'] ?? 'Foundation capture failed.'), 'status' => $foundationFailure['status'] ?? null, ]); } } } if ($didMutateBackupSet) { $allFailures = array_merge($existingBackupFailures, $newBackupFailures); $mutations = $backupSetItemMutations + $foundationMutations; $backupSetStatus = match (true) { $mutations === 0 && count($allFailures) > 0 => 'failed', count($allFailures) > 0 => 'partial', default => 'completed', }; $backupSet->update([ 'status' => $backupSetStatus, 'item_count' => $backupSet->items()->count(), 'completed_at' => now(), 'metadata' => ['failures' => $allFailures], ]); } $bulkOperationService->complete($run); if (! $run->user) { return; } $message = "Added {$run->succeeded} policies"; if ($run->skipped > 0) { $message .= " ({$run->skipped} skipped)"; } if ($run->failed > 0) { $message .= " ({$run->failed} failed)"; } if ($this->includeFoundations) { $message .= ". Foundations: {$foundationMutations} items"; if ($foundationFailures > 0) { $message .= " ({$foundationFailures} failed)"; } } $message .= '.'; $notification = Notification::make() ->title($run->failed > 0 ? 'Add Policies Completed (partial)' : 'Add Policies Completed') ->body($message) ->actions([ \Filament\Actions\Action::make('view_run') ->label('View run') ->url(BulkOperationRunResource::getUrl('view', ['record' => $run], tenant: $tenant)), ]); if ($run->failed > 0) { $notification->warning(); } else { $notification->success(); } $notification ->sendToDatabase($run->user) ->send(); } /** * @return array */ private function extractPolicyIds(BulkOperationRun $run): array { $itemIds = $run->item_ids ?? []; $policyIds = []; if (is_array($itemIds) && array_key_exists('policy_ids', $itemIds) && is_array($itemIds['policy_ids'])) { $policyIds = $itemIds['policy_ids']; } elseif (is_array($itemIds)) { $policyIds = $itemIds; } $policyIds = array_values(array_unique(array_map('intval', $policyIds))); $policyIds = array_values(array_filter($policyIds, fn (int $value): bool => $value > 0)); sort($policyIds); return $policyIds; } /** * @param array $entry */ private function appendRunFailure(BulkOperationRun $run, array $entry): void { $failures = $run->failures ?? []; $failures[] = array_merge([ 'timestamp' => now()->toIso8601String(), ], $entry); $run->update(['failures' => $failures]); } private function mapGraphFailureReasonCode(?int $status): string { return match (true) { $status === 403 => 'graph_forbidden', in_array($status, [429, 503], true) => 'graph_throttled', in_array($status, [408, 500, 502, 504], true) => 'graph_transient', default => 'unknown', }; } /** * @return array{0:array{created:int,restored:int,failures:array},1:array} */ private function captureFoundations( BulkOperationService $bulkOperationService, FoundationSnapshotService $foundationSnapshots, Tenant $tenant, BackupSet $backupSet, ): array { $types = config('tenantpilot.foundation_types', []); $created = 0; $restored = 0; $failures = []; foreach ($types as $typeConfig) { $foundationType = $typeConfig['type'] ?? null; if (! is_string($foundationType) || $foundationType === '') { continue; } $result = $foundationSnapshots->fetchAll($tenant, $foundationType); foreach (($result['failures'] ?? []) as $failure) { if (! is_array($failure)) { continue; } $status = isset($failure['status']) && is_numeric($failure['status']) ? (int) $failure['status'] : null; $reasonCode = $this->mapGraphFailureReasonCode($status); $reason = $bulkOperationService->sanitizeFailureReason((string) ($failure['reason'] ?? 'Foundation capture failed.')); $failures[] = [ 'foundation_type' => $foundationType, 'reason' => $reason, 'status' => $status, 'reason_code' => $reasonCode, ]; } foreach (($result['items'] ?? []) as $snapshot) { if (! is_array($snapshot)) { continue; } $sourceId = $snapshot['source_id'] ?? null; if (! is_string($sourceId) || $sourceId === '') { continue; } $existing = BackupItem::withTrashed() ->where('backup_set_id', $backupSet->getKey()) ->where('policy_type', $foundationType) ->where('policy_identifier', $sourceId) ->first(); if ($existing) { if ($existing->trashed()) { $existing->restore(); $restored++; } continue; } BackupItem::create([ 'tenant_id' => $tenant->getKey(), 'backup_set_id' => $backupSet->getKey(), 'policy_id' => null, 'policy_identifier' => $sourceId, 'policy_type' => $foundationType, 'platform' => $typeConfig['platform'] ?? null, 'payload' => $snapshot['payload'] ?? [], 'metadata' => $snapshot['metadata'] ?? [], ]); $created++; } } return [ [ 'created' => $created, 'restored' => $restored, 'failures' => $failures, ], $failures, ]; } }