TenantAtlas/apps/platform/app/Services/TenantConfiguration/CoverageResourceIdentityEvaluator.php
Ahmed Darrazi 5ceecdeb62
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 6m40s
feat: implement canonical identity engine
2026-06-26 08:46:18 +02:00

146 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TenantConfiguration;
use App\Models\ManagedEnvironment;
use App\Models\ProviderConnection;
use App\Models\TenantConfigurationResource;
use App\Models\TenantConfigurationResourceType;
use App\Support\TenantConfiguration\ClaimState;
use App\Support\TenantConfiguration\IdentityState;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
final class CoverageResourceIdentityEvaluator
{
public function __construct(
private readonly IdentityConflictDiagnosticsBuilder $diagnostics,
) {}
public function evaluate(
CanonicalIdentityResult $result,
ManagedEnvironment $tenant,
ProviderConnection $providerConnection,
TenantConfigurationResourceType $resourceType,
): CanonicalIdentityResult {
if (! $this->requiresUnsafeCollisionEvaluation($result)) {
return $result;
}
$candidates = $this->sameCandidateRows($result, $tenant, $providerConnection, $resourceType);
if ($candidates->isEmpty()) {
return $result;
}
$matching = $candidates->first(
fn (TenantConfigurationResource $resource): bool => data_get($resource->source_identity, 'fingerprint') === $result->fingerprint(),
);
if ($result->identityState === IdentityState::Derived
&& $matching instanceof TenantConfigurationResource
&& $candidates->count() === 1
) {
return $result->withCanonicalResourceKey((string) $matching->canonical_resource_key);
}
$candidateCount = $candidates->count() + $this->incomingCandidateCount($result, $matching);
$existingIds = $candidates
->pluck('id')
->map(static fn (mixed $id): int => (int) $id)
->values()
->all();
$conflictDiagnostics = $this->diagnostics->build(
reasonCode: $this->collisionReasonCode($result),
identityState: IdentityState::IdentityConflict,
keyKind: $result->keyKind,
metadata: [
'candidate_key_hash' => $result->candidateKeyHash(),
'candidate_count' => $candidateCount,
'conflicting_resource_ids' => $existingIds,
],
);
$candidates->each(function (TenantConfigurationResource $resource) use ($conflictDiagnostics): void {
$resource->forceFill([
'latest_identity_state' => IdentityState::IdentityConflict->value,
'latest_claim_state' => ClaimState::ClaimBlocked->value,
'identity_diagnostics' => array_replace_recursive(
is_array($resource->identity_diagnostics) ? $resource->identity_diagnostics : [],
$conflictDiagnostics,
),
'identity_evaluated_at' => now(),
])->save();
});
$canonicalKey = $result->identityState === IdentityState::Derived && $matching instanceof TenantConfigurationResource
? (string) $matching->canonical_resource_key
: $this->newConflictResourceKey($result);
return $result->asConflict($canonicalKey, $conflictDiagnostics);
}
private function requiresUnsafeCollisionEvaluation(CanonicalIdentityResult $result): bool
{
return in_array($result->identityState, [
IdentityState::Derived,
IdentityState::MissingExternalId,
IdentityState::UnsupportedIdentity,
], true);
}
private function incomingCandidateCount(
CanonicalIdentityResult $result,
?TenantConfigurationResource $matching,
): int {
if ($result->identityState !== IdentityState::Derived) {
return 1;
}
return $matching instanceof TenantConfigurationResource ? 0 : 1;
}
private function collisionReasonCode(CanonicalIdentityResult $result): string
{
return match ($result->identityState) {
IdentityState::MissingExternalId => 'same_scope_missing_identity_collision',
IdentityState::UnsupportedIdentity => 'same_scope_unsupported_identity_collision',
default => 'same_scope_derived_identity_collision',
};
}
private function newConflictResourceKey(CanonicalIdentityResult $result): string
{
return sprintf(
'%s:conflict:%s:%s',
$result->canonicalResourceKey,
substr($result->fingerprint(), 0, 16),
Str::ulid()->toBase32(),
);
}
/**
* @return Collection<int, TenantConfigurationResource>
*/
private function sameCandidateRows(
CanonicalIdentityResult $result,
ManagedEnvironment $tenant,
ProviderConnection $providerConnection,
TenantConfigurationResourceType $resourceType,
): Collection {
return TenantConfigurationResource::query()
->where('workspace_id', (int) $tenant->workspace_id)
->where('managed_environment_id', (int) $tenant->getKey())
->where('provider_connection_id', (int) $providerConnection->getKey())
->where('resource_type_id', (int) $resourceType->getKey())
->where(function ($query) use ($result): void {
$query
->where('canonical_resource_key', $result->canonicalResourceKey)
->orWhere('canonical_resource_key', 'like', $result->canonicalResourceKey.':conflict:%');
})
->get();
}
}