TenantAtlas/apps/platform/app/Services/TenantConfiguration/ExchangePowerShellRuntimeReadinessChecker.php
Ahmed Darrazi 90a411429e
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m23s
feat: add Exchange PowerShell production runner gate
2026-07-07 20:32:36 +02:00

138 lines
5.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TenantConfiguration;
use App\Support\Providers\ProviderReasonCodes;
use Throwable;
final class ExchangePowerShellRuntimeReadinessChecker
{
public function __construct(
private readonly ExchangePowerShellProcessExecutor $executor,
) {}
public function check(?ExchangePowerShellRuntimePolicy $policy = null): ExchangePowerShellGateResult
{
$policy ??= ExchangePowerShellRuntimePolicy::fromConfig();
try {
if (! $policy->invocationEnabled) {
return $this->blocked('runtime_blocked_feature_disabled', [
'runtime_state' => 'blocked_feature_disabled',
]);
}
if (! $policy->productionRunnerEnabled) {
return $this->blocked('runtime_blocked_runner_disabled', [
'runtime_state' => 'blocked_runner_disabled',
]);
}
if (! $policy->environmentAllowed()) {
return $this->blocked('runtime_blocked_environment_not_allowed', [
'runtime_state' => 'environment_not_allowed',
'runtime_environment' => $policy->currentEnvironment,
]);
}
if (! $policy->timeoutPolicyValid()) {
return $this->blocked('runtime_blocked_timeout_policy_missing', [
'runtime_state' => 'timeout_policy_missing',
]);
}
if (! $policy->tempPolicySafe()) {
return $this->blocked('runtime_blocked_temp_policy_unsafe', [
'runtime_state' => 'temp_policy_unsafe',
]);
}
if (! $this->executor->available()) {
return $this->blocked('runtime_blocked_process_executor_missing', [
'runtime_state' => 'process_executor_missing',
]);
}
$powershell = $this->executor->run(new ExchangePowerShellProcessCommand(
arguments: [
$policy->powershellBinary,
'-NoLogo',
'-NoProfile',
'-NonInteractive',
'-Command',
'$PSVersionTable.PSVersion.ToString()',
],
timeoutSeconds: $policy->checkTimeoutSeconds,
stdoutMaxBytes: 4096,
stderrMaxBytes: 4096,
));
if ($powershell->timedOut || $powershell->exitCode !== 0 || trim($powershell->stdout) === '') {
return $this->blocked('runtime_blocked_powershell_missing', [
'runtime_state' => 'powershell_missing',
]);
}
if (! $policy->moduleCheckEnabled) {
return ExchangePowerShellGateResult::allowed([
'runtime_state' => 'ready',
'module_state' => 'unchecked',
]);
}
$module = $this->executor->run(new ExchangePowerShellProcessCommand(
arguments: [
$policy->powershellBinary,
'-NoLogo',
'-NoProfile',
'-NonInteractive',
'-Command',
'Get-Module -ListAvailable -Name ExchangeOnlineManagement',
],
timeoutSeconds: $policy->checkTimeoutSeconds,
stdoutMaxBytes: 4096,
stderrMaxBytes: 4096,
));
if ($module->timedOut || $module->exitCode !== 0) {
return $this->blocked('runtime_blocked_exchange_module_unknown', [
'runtime_state' => 'exchange_module_unknown',
]);
}
if (! str_contains(trim($module->stdout), 'ExchangeOnlineManagement')) {
return $this->blocked('runtime_blocked_exchange_module_missing', [
'runtime_state' => 'exchange_module_missing',
]);
}
return ExchangePowerShellGateResult::allowed([
'runtime_state' => 'ready',
'module_state' => 'available',
]);
} catch (Throwable) {
return ExchangePowerShellGateResult::blocked(
reasonCode: ProviderReasonCodes::UnknownError,
failureCode: 'runtime_failed_unexpected',
message: 'Exchange PowerShell runtime readiness failed safely.',
context: ['runtime_state' => 'unexpected_failure'],
);
}
}
/**
* @param array<string, mixed> $context
*/
private function blocked(string $failureCode, array $context): ExchangePowerShellGateResult
{
return ExchangePowerShellGateResult::blocked(
reasonCode: ProviderReasonCodes::ProviderBindingUnsupported,
failureCode: $failureCode,
message: 'Exchange PowerShell runtime readiness is not satisfied.',
context: $context,
);
}
}