TenantAtlas/apps/platform/app/Jobs/TenantConfiguration/CaptureTenantConfigurationEvidenceJob.php
Ahmed Darrazi 736e61c73e
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m37s
feat: add generic content-backed coverage capture
2026-06-25 21:55:27 +02:00

226 lines
7.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Jobs\TenantConfiguration;
use App\Jobs\Concerns\BridgesFailedOperationRun;
use App\Jobs\Middleware\EnsureQueuedExecutionLegitimate;
use App\Jobs\Middleware\TrackOperationRun;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Services\Audit\AuditRecorder;
use App\Services\OperationRunService;
use App\Services\TenantConfiguration\GenericContentEvidenceCaptureService;
use App\Support\Audit\AuditActorSnapshot;
use App\Support\Audit\AuditOutcome;
use App\Support\Audit\AuditTargetSnapshot;
use App\Support\OpsUx\RunFailureSanitizer;
use App\Support\OperationRunStatus;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use RuntimeException;
use Throwable;
class CaptureTenantConfigurationEvidenceJob implements ShouldQueue
{
use BridgesFailedOperationRun;
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public int $timeout = 300;
public bool $failOnTimeout = true;
public ?OperationRun $operationRun = null;
public function __construct(
public OperationRun $run,
) {
$this->operationRun = $run;
}
/**
* @return array<int, object>
*/
public function middleware(): array
{
return [
new EnsureQueuedExecutionLegitimate,
new TrackOperationRun,
];
}
public function getOperationRun(): ?OperationRun
{
return $this->operationRun;
}
public function getOperationRunId(): int
{
return (int) $this->run->getKey();
}
public function handle(
GenericContentEvidenceCaptureService $captureService,
OperationRunService $operationRuns,
AuditRecorder $auditRecorder,
): void {
$run = $this->run->fresh(['tenant.workspace', 'user']);
if (! $run instanceof OperationRun) {
throw new RuntimeException('OperationRun context is required for tenant configuration capture.');
}
$this->operationRun = $run;
if ($run->status === OperationRunStatus::Completed->value) {
return;
}
$tenant = $run->tenant;
$providerConnectionId = (int) data_get($run->context, 'target_scope.provider_connection_id', 0);
$providerConnection = ProviderConnection::query()
->whereKey($providerConnectionId)
->where('workspace_id', (int) $run->workspace_id)
->where('managed_environment_id', (int) $run->managed_environment_id)
->first();
if (! $tenant || ! $providerConnection instanceof ProviderConnection) {
throw new RuntimeException('Tenant configuration capture run is missing its managed environment or same-scope provider connection.');
}
try {
$result = $captureService->capture(
tenant: $tenant,
providerConnection: $providerConnection,
operationRun: $run,
canonicalTypes: $this->canonicalTypes($run),
);
$run->forceFill([
'context' => $this->contextWithCaptureResult($run, $result['outcomes']),
])->save();
$completed = $operationRuns->updateRun(
run: $run,
status: OperationRunStatus::Completed->value,
outcome: $result['run_outcome'],
summaryCounts: $result['summary_counts'],
failures: $result['failures'],
);
$this->recordTerminalAudit($auditRecorder, $completed, $providerConnection, $result);
} catch (Throwable $e) {
$this->recordFailureAudit($auditRecorder, $run, $providerConnection, $e);
throw $e;
}
}
/**
* @return list<string>|null
*/
private function canonicalTypes(OperationRun $run): ?array
{
$types = data_get($run->context, 'resource_types');
if (! is_array($types)) {
return null;
}
return collect($types)
->filter(static fn (mixed $type): bool => is_string($type) && trim($type) !== '')
->map(static fn (string $type): string => trim($type))
->values()
->all();
}
/**
* @param list<array<string, mixed>> $outcomes
* @return array<string, mixed>
*/
private function contextWithCaptureResult(OperationRun $run, array $outcomes): array
{
$context = is_array($run->context) ? $run->context : [];
$context['capture'] = [
'resource_type_outcomes' => $outcomes,
'completed_at' => now()->toJSON(),
];
return $context;
}
/**
* @param array{outcomes: list<array<string, mixed>>, summary_counts: array<string, int>, run_outcome: string, failures: list<array<string, mixed>>} $result
*/
private function recordTerminalAudit(
AuditRecorder $auditRecorder,
OperationRun $run,
ProviderConnection $providerConnection,
array $result,
): void {
$tenant = $run->tenant;
if (! $tenant) {
return;
}
$auditRecorder->record(
action: $result['run_outcome'] === 'failed'
? 'tenant_configuration.capture.failed'
: 'tenant_configuration.capture.completed',
context: [
'metadata' => [
'operation_run_id' => (int) $run->getKey(),
'provider_connection_id' => (int) $providerConnection->getKey(),
'resource_type_outcomes' => $result['outcomes'],
'summary_counts' => $result['summary_counts'],
],
],
workspace: $tenant->workspace,
tenant: $tenant,
actor: $run->user ? AuditActorSnapshot::human($run->user) : null,
target: new AuditTargetSnapshot('operation_run', (int) $run->getKey()),
outcome: AuditOutcome::normalize($result['run_outcome']),
operationRunId: (int) $run->getKey(),
);
}
private function recordFailureAudit(
AuditRecorder $auditRecorder,
OperationRun $run,
ProviderConnection $providerConnection,
Throwable $e,
): void {
$tenant = $run->tenant;
if (! $tenant) {
return;
}
$auditRecorder->record(
action: 'tenant_configuration.capture.failed',
context: [
'metadata' => [
'operation_run_id' => (int) $run->getKey(),
'provider_connection_id' => (int) $providerConnection->getKey(),
'reason_code' => 'capture_exception',
'message' => RunFailureSanitizer::sanitizeMessage($e->getMessage()),
],
],
workspace: $tenant->workspace,
tenant: $tenant,
actor: $run->user ? AuditActorSnapshot::human($run->user) : null,
target: new AuditTargetSnapshot('operation_run', (int) $run->getKey()),
outcome: AuditOutcome::Failed,
operationRunId: (int) $run->getKey(),
);
}
}