with('integrationToken') ->whereKey($this->zoneId) ->whereHas('integrationToken', fn ($query) => $query->where('team_id', $this->teamId)) ->firstOrFail(); if (! DnsRecordHints::isPublicAddress($this->content)) { $this->finished($zone, false, DnsRecordHints::NO_PUBLIC_ADDRESS_MESSAGE); return; } $resource = $this->resource(); try { try { $provider->createRecord($zone, $this->hostname, $this->content, $resource); $message = "DNS record added for {$this->hostname}."; } catch (DnsRecordConflictException $conflict) { if (! $this->isUnusedCoolifyRecord($zone, $conflict, $resource)) { $this->finished($zone, false, $conflict->getMessage(), [ 'zone_id' => $zone->id, 'record_id' => $conflict->providerRecordId, 'current' => $conflict->currentValue, 'proposed' => $conflict->proposedValue, ]); return; } $provider->replaceRecord($zone, $conflict->providerRecordId, $this->hostname, $this->content, $resource, $conflict->currentValue); $message = "DNS record replaced for {$this->hostname}. It pointed to {$conflict->currentValue}, which no other resource uses."; } } catch (LockTimeoutException) { if ($this->attempts() < $this->tries) { $this->release(10 * $this->attempts()); return; } $this->finished($zone, false, 'Another DNS change for this hostname is still in progress. Try again later.'); return; } catch (Throwable $exception) { $this->finished($zone, false, $exception->getMessage()); return; } $this->finished($zone, true, $message); } /** * True when Coolify created the record (owned, and the provider comment still proves it) and no resource other * than this one references the record or uses the hostname. */ private function isUnusedCoolifyRecord(DnsProviderZone $zone, DnsRecordConflictException $conflict, ?Model $resource): bool { if ($resource === null) { return false; } $record = ManagedDnsRecord::query()->with('references.resource') ->where('dns_provider_zone_id', $zone->id) ->where('provider_record_id', $conflict->providerRecordId) ->first(); if ($record === null || ! $record->owned || $conflict->currentComment !== $record->ownershipComment()) { return false; } $resourceKey = $resource->getMorphClass().'|'.$resource->getKey(); $usedByOtherResource = $record->references->contains(fn (ManagedDnsRecordReference $reference): bool => $reference->resource !== null && $reference->resource_type.'|'.$reference->resource_id !== $resourceKey); return ! $usedByOtherResource && app(ManagedDnsRecordCleanup::class)->resourcesUsingHostname($this->hostname, [$resourceKey])->isEmpty(); } /** @param array{zone_id: int, record_id: string, current: string, proposed: string}|null $conflict */ private function finished(DnsProviderZone $zone, bool $successful, string $message, ?array $conflict = null): void { DnsRecordConfigurationFinished::dispatch( $this->teamId, $this->resourceType, $this->resourceId, $this->hostname, $successful, $zone->integrationToken->name, $message, $conflict, ); } public function failed(?Throwable $exception): void { DnsRecordConfigurationFinished::dispatch( $this->teamId, $this->resourceType, $this->resourceId, $this->hostname, false, '', 'The DNS zone is no longer available.', ); } private function resource(): ?Model { $resourceClass = $this->resourceType === null ? null : (Relation::getMorphedModel($this->resourceType) ?? $this->resourceType); if ($resourceClass === null || $this->resourceId === null || ! is_subclass_of($resourceClass, Model::class)) { return null; } return $resourceClass::query()->find($this->resourceId); } }