Automated PR provided by Codex via Gitea API. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #484
146 lines
5.4 KiB
PHP
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();
|
|
}
|
|
}
|