Deploy Ladill Hosting / deploy (push) Failing after 17s
Shared web hosting extracted from the platform monolith, with CI deploy to /var/www/ladill-hosting matching Bird/Domains/Email. Co-authored-by: Cursor <cursoragent@cursor.com>
558 lines
20 KiB
PHP
558 lines
20 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Hosting;
|
|
|
|
use App\Models\Domain;
|
|
use App\Models\ServerDatabase;
|
|
use App\Models\ServerFile;
|
|
use App\Models\ServerSite;
|
|
use App\Models\ServerTask;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Str;
|
|
|
|
class ServerTaskResultService
|
|
{
|
|
public function apply(ServerTask $task): ServerTask
|
|
{
|
|
$task->loadMissing('order', 'agent');
|
|
|
|
$references = match ($task->type) {
|
|
ServerTask::TYPE_DOMAIN_ADD => $this->persistDomainTask($task),
|
|
ServerTask::TYPE_SITE_DELETE => $this->persistSiteDeleteTask($task),
|
|
ServerTask::TYPE_SSL_REQUEST => $this->persistSslTask($task),
|
|
ServerTask::TYPE_SSL_RENEW => $this->persistSslRenewTask($task),
|
|
ServerTask::TYPE_DATABASE_CREATE => $this->persistDatabaseTask($task),
|
|
ServerTask::TYPE_PHP_CONFIGURE => $this->persistPhpTask($task),
|
|
ServerTask::TYPE_CRON_LIST,
|
|
ServerTask::TYPE_CRON_UPSERT,
|
|
ServerTask::TYPE_CRON_REMOVE => $this->persistCronTask($task),
|
|
ServerTask::TYPE_LOG_READ => $this->persistLogTask($task),
|
|
ServerTask::TYPE_FILE_LIST,
|
|
ServerTask::TYPE_FILE_READ,
|
|
ServerTask::TYPE_FILE_WRITE => $this->persistFileTask($task),
|
|
default => [],
|
|
};
|
|
|
|
if ($references !== []) {
|
|
$result = (array) ($task->result ?? []);
|
|
$result['model_refs'] = array_merge((array) ($result['model_refs'] ?? []), $references);
|
|
$task->forceFill(['result' => $result])->save();
|
|
}
|
|
|
|
return $task->fresh();
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function persistDomainTask(ServerTask $task): array
|
|
{
|
|
$result = (array) ($task->result ?? []);
|
|
$domainName = Str::lower((string) ($result['domain'] ?? data_get($task->payload, 'domain', '')));
|
|
if ($domainName === '') {
|
|
return [];
|
|
}
|
|
|
|
$order = $task->order()->first();
|
|
$domain = Domain::query()->where('host', $domainName)->first();
|
|
|
|
if ($domain === null && $order !== null) {
|
|
$domain = Domain::create([
|
|
'user_id' => $order->user_id,
|
|
'host' => $domainName,
|
|
'type' => 'server',
|
|
'source' => 'server_manager',
|
|
'status' => 'pending',
|
|
'onboarding_mode' => Domain::MODE_MANUAL_DNS,
|
|
'onboarding_state' => Domain::STATE_CONNECTED,
|
|
'dns_mode' => 'manual',
|
|
'connected_at' => now(),
|
|
'verification_meta' => [
|
|
'source' => 'server_agent_task',
|
|
'customer_hosting_order_id' => $task->customer_hosting_order_id,
|
|
],
|
|
]);
|
|
} elseif ($domain !== null) {
|
|
$verificationMeta = array_merge((array) ($domain->verification_meta ?? []), [
|
|
'server_agent_task_id' => $task->id,
|
|
'customer_hosting_order_id' => $task->customer_hosting_order_id,
|
|
]);
|
|
|
|
$domain->fill([
|
|
'user_id' => $domain->user_id ?: $order?->user_id,
|
|
'source' => $domain->source ?: 'server_manager',
|
|
'connected_at' => $domain->connected_at ?? now(),
|
|
'verification_meta' => $verificationMeta,
|
|
])->save();
|
|
}
|
|
|
|
$site = ServerSite::query()->updateOrCreate(
|
|
[
|
|
'customer_hosting_order_id' => $task->customer_hosting_order_id,
|
|
'domain' => $domainName,
|
|
],
|
|
[
|
|
'server_agent_id' => $task->server_agent_id,
|
|
'latest_server_task_id' => $task->id,
|
|
'domain_id' => $domain?->id,
|
|
'document_root' => (string) ($result['document_root'] ?? data_get($task->payload, 'document_root', '')) ?: null,
|
|
'php_version' => (string) data_get($task->payload, 'php_version', '') ?: null,
|
|
'php_socket' => (string) ($result['php_socket'] ?? '') ?: null,
|
|
'status' => 'active',
|
|
'metadata' => array_filter([
|
|
'task_type' => $task->type,
|
|
'server_task_id' => $task->id,
|
|
'payload' => $task->payload,
|
|
'result' => Arr::except($result, ['message', 'model_refs']),
|
|
], static fn ($value): bool => $value !== null && $value !== []),
|
|
]
|
|
);
|
|
|
|
return array_filter([
|
|
'domain_id' => $domain?->id,
|
|
'server_site_id' => $site->id,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function persistSiteDeleteTask(ServerTask $task): array
|
|
{
|
|
$result = (array) ($task->result ?? []);
|
|
$domainName = Str::lower((string) ($result['domain'] ?? data_get($task->payload, 'domain', '')));
|
|
if ($domainName === '') {
|
|
return [];
|
|
}
|
|
|
|
$site = ServerSite::query()
|
|
->where('customer_hosting_order_id', $task->customer_hosting_order_id)
|
|
->where('domain', $domainName)
|
|
->first();
|
|
|
|
if ($site === null) {
|
|
return [];
|
|
}
|
|
|
|
$metadata = array_merge((array) ($site->metadata ?? []), [
|
|
'removed_at' => now()->toIso8601String(),
|
|
'removed_by_task_id' => $task->id,
|
|
]);
|
|
|
|
$site->fill([
|
|
'server_agent_id' => $task->server_agent_id,
|
|
'latest_server_task_id' => $task->id,
|
|
'status' => 'removed',
|
|
'ssl_status' => null,
|
|
'metadata' => $metadata,
|
|
])->save();
|
|
|
|
return [
|
|
'server_site_id' => $site->id,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function persistSslTask(ServerTask $task): array
|
|
{
|
|
$result = (array) ($task->result ?? []);
|
|
$domainName = Str::lower((string) ($result['domain'] ?? data_get($task->payload, 'domain', '')));
|
|
if ($domainName === '') {
|
|
return [];
|
|
}
|
|
|
|
$site = ServerSite::query()
|
|
->where('customer_hosting_order_id', $task->customer_hosting_order_id)
|
|
->where('domain', $domainName)
|
|
->first();
|
|
|
|
if ($site === null) {
|
|
$site = ServerSite::create([
|
|
'customer_hosting_order_id' => $task->customer_hosting_order_id,
|
|
'server_agent_id' => $task->server_agent_id,
|
|
'latest_server_task_id' => $task->id,
|
|
'domain' => $domainName,
|
|
'status' => 'active',
|
|
'ssl_status' => (string) ($result['ssl'] ?? 'issued'),
|
|
'ssl_provisioned_at' => now(),
|
|
'metadata' => [
|
|
'created_from' => 'ssl.request',
|
|
'server_task_id' => $task->id,
|
|
],
|
|
]);
|
|
} else {
|
|
$site->fill([
|
|
'server_agent_id' => $task->server_agent_id,
|
|
'latest_server_task_id' => $task->id,
|
|
'ssl_status' => (string) ($result['ssl'] ?? 'issued'),
|
|
'ssl_provisioned_at' => now(),
|
|
])->save();
|
|
}
|
|
|
|
if ($site->domain_id) {
|
|
$domain = Domain::query()->find($site->domain_id);
|
|
} else {
|
|
$domain = Domain::query()->where('host', $domainName)->first();
|
|
}
|
|
|
|
if ($domain !== null) {
|
|
$domain->fill([
|
|
'ssl_status' => (string) ($result['ssl'] ?? 'issued'),
|
|
'ssl_provisioned_at' => now(),
|
|
])->save();
|
|
|
|
if ($site->domain_id === null) {
|
|
$site->forceFill(['domain_id' => $domain->id])->save();
|
|
}
|
|
}
|
|
|
|
return array_filter([
|
|
'domain_id' => $domain->id ?? null,
|
|
'server_site_id' => $site->id,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function persistSslRenewTask(ServerTask $task): array
|
|
{
|
|
$expiresAt = now()->addDays(90);
|
|
|
|
ServerSite::query()
|
|
->where('customer_hosting_order_id', $task->customer_hosting_order_id)
|
|
->whereIn('ssl_status', ['issued', 'active'])
|
|
->update([
|
|
'ssl_expires_at' => $expiresAt,
|
|
'ssl_provisioned_at' => now(),
|
|
]);
|
|
|
|
$domainIds = ServerSite::query()
|
|
->where('customer_hosting_order_id', $task->customer_hosting_order_id)
|
|
->whereNotNull('domain_id')
|
|
->pluck('domain_id');
|
|
|
|
if ($domainIds->isNotEmpty()) {
|
|
Domain::query()
|
|
->whereIn('id', $domainIds)
|
|
->where('ssl_status', 'active')
|
|
->update(['ssl_expires_at' => $expiresAt]);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function persistDatabaseTask(ServerTask $task): array
|
|
{
|
|
$result = (array) ($task->result ?? []);
|
|
$databaseName = (string) ($result['database'] ?? data_get($task->payload, 'name', ''));
|
|
if ($databaseName === '') {
|
|
return [];
|
|
}
|
|
|
|
$database = ServerDatabase::query()->updateOrCreate(
|
|
[
|
|
'customer_hosting_order_id' => $task->customer_hosting_order_id,
|
|
'name' => $databaseName,
|
|
],
|
|
[
|
|
'server_agent_id' => $task->server_agent_id,
|
|
'latest_server_task_id' => $task->id,
|
|
'username' => (string) ($result['username'] ?? $databaseName) ?: null,
|
|
'password_encrypted' => isset($result['password']) && $result['password'] !== '' ? encrypt((string) $result['password']) : null,
|
|
'engine' => 'mariadb',
|
|
'status' => 'active',
|
|
'metadata' => array_filter([
|
|
'server_task_id' => $task->id,
|
|
'payload' => $task->payload,
|
|
'result' => Arr::except($result, ['message', 'model_refs', 'password']),
|
|
], static fn ($value): bool => $value !== null && $value !== []),
|
|
]
|
|
);
|
|
|
|
return [
|
|
'server_database_id' => $database->id,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function persistPhpTask(ServerTask $task): array
|
|
{
|
|
$result = (array) ($task->result ?? []);
|
|
$domainName = Str::lower((string) ($result['domain'] ?? data_get($task->payload, 'domain', '')));
|
|
if ($domainName === '') {
|
|
return [];
|
|
}
|
|
|
|
$site = ServerSite::query()->updateOrCreate(
|
|
[
|
|
'customer_hosting_order_id' => $task->customer_hosting_order_id,
|
|
'domain' => $domainName,
|
|
],
|
|
[
|
|
'server_agent_id' => $task->server_agent_id,
|
|
'latest_server_task_id' => $task->id,
|
|
'php_version' => (string) ($result['php_version'] ?? data_get($task->payload, 'php_version', '')) ?: null,
|
|
'php_socket' => (string) ($result['php_socket'] ?? '') ?: null,
|
|
'status' => 'active',
|
|
'metadata' => array_filter([
|
|
'php_configured_at' => now()->toIso8601String(),
|
|
'server_task_id' => $task->id,
|
|
'result' => Arr::except($result, ['message', 'model_refs']),
|
|
], static fn ($value): bool => $value !== null && $value !== []),
|
|
]
|
|
);
|
|
|
|
return [
|
|
'server_site_id' => $site->id,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function persistCronTask(ServerTask $task): array
|
|
{
|
|
$entries = array_values(array_filter((array) data_get($task->result, 'entries', []), 'is_array'));
|
|
$order = $task->order()->first();
|
|
|
|
if ($order !== null) {
|
|
$meta = (array) ($order->meta ?? []);
|
|
$panelState = (array) ($meta['server_panel'] ?? []);
|
|
$panelState['managed_cron_entries'] = $entries;
|
|
$panelState['managed_cron_synced_at'] = now()->toIso8601String();
|
|
$meta['server_panel'] = $panelState;
|
|
$order->update(['meta' => $meta]);
|
|
}
|
|
|
|
return [
|
|
'cron_entry_count' => count($entries),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function persistLogTask(ServerTask $task): array
|
|
{
|
|
$result = (array) ($task->result ?? []);
|
|
$contentBase64 = (string) ($result['content_base64'] ?? '');
|
|
$content = $contentBase64 !== '' ? base64_decode($contentBase64, true) : false;
|
|
$content = is_string($content) ? $content : '';
|
|
$target = (string) ($result['target'] ?? data_get($task->payload, 'target', 'log'));
|
|
$domain = (string) ($result['domain'] ?? data_get($task->payload, 'domain', ''));
|
|
$snapshotKey = $target.($domain !== '' ? ':'.$domain : '');
|
|
$order = $task->order()->first();
|
|
|
|
if ($order !== null) {
|
|
$meta = (array) ($order->meta ?? []);
|
|
$panelState = (array) ($meta['server_panel'] ?? []);
|
|
$panelState['log_snapshots'] = array_merge((array) ($panelState['log_snapshots'] ?? []), [
|
|
$snapshotKey => [
|
|
'target' => $target,
|
|
'domain' => $domain !== '' ? $domain : null,
|
|
'lines' => (int) ($result['lines'] ?? data_get($task->payload, 'lines', 0)),
|
|
'path' => $result['path'] ?? null,
|
|
'read_at' => now()->toIso8601String(),
|
|
'content_preview' => $this->previewForContent($content),
|
|
],
|
|
]);
|
|
$meta['server_panel'] = $panelState;
|
|
$order->update(['meta' => $meta]);
|
|
}
|
|
|
|
return [
|
|
'log_snapshot_key' => $snapshotKey,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function persistFileTask(ServerTask $task): array
|
|
{
|
|
$result = (array) ($task->result ?? []);
|
|
|
|
return match ($task->type) {
|
|
ServerTask::TYPE_FILE_LIST => $this->persistFileListTask($task, $result),
|
|
ServerTask::TYPE_FILE_READ => $this->persistFileReadTask($task, $result),
|
|
ServerTask::TYPE_FILE_WRITE => $this->persistFileWriteTask($task, $result),
|
|
default => [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $result
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function persistFileListTask(ServerTask $task, array $result): array
|
|
{
|
|
$basePath = (string) data_get($task->payload, 'path', '');
|
|
$entries = array_values(array_filter((array) ($result['entries'] ?? []), 'is_array'));
|
|
$recordIds = [];
|
|
|
|
if ($basePath !== '') {
|
|
$rootRecord = ServerFile::query()->updateOrCreate(
|
|
[
|
|
'customer_hosting_order_id' => $task->customer_hosting_order_id,
|
|
'path' => $basePath,
|
|
],
|
|
[
|
|
'server_agent_id' => $task->server_agent_id,
|
|
'latest_server_task_id' => $task->id,
|
|
'kind' => 'directory',
|
|
'exists' => true,
|
|
'metadata' => [
|
|
'entry_count' => count($entries),
|
|
'server_task_id' => $task->id,
|
|
],
|
|
'last_synced_at' => now(),
|
|
]
|
|
);
|
|
$recordIds[] = $rootRecord->id;
|
|
}
|
|
|
|
foreach ($entries as $entry) {
|
|
$path = (string) ($entry['path'] ?? '');
|
|
if ($path === '') {
|
|
continue;
|
|
}
|
|
|
|
$record = ServerFile::query()->updateOrCreate(
|
|
[
|
|
'customer_hosting_order_id' => $task->customer_hosting_order_id,
|
|
'path' => $path,
|
|
],
|
|
[
|
|
'server_agent_id' => $task->server_agent_id,
|
|
'latest_server_task_id' => $task->id,
|
|
'kind' => $this->normalizeFileKind((string) ($entry['type'] ?? 'f')),
|
|
'exists' => true,
|
|
'size_bytes' => is_numeric($entry['size'] ?? null) ? (int) $entry['size'] : null,
|
|
'metadata' => [
|
|
'listed_name' => $entry['name'] ?? basename($path),
|
|
'server_task_id' => $task->id,
|
|
],
|
|
'last_synced_at' => now(),
|
|
]
|
|
);
|
|
|
|
$recordIds[] = $record->id;
|
|
}
|
|
|
|
return [
|
|
'server_file_ids' => array_values(array_unique($recordIds)),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $result
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function persistFileReadTask(ServerTask $task, array $result): array
|
|
{
|
|
$path = (string) ($result['path'] ?? data_get($task->payload, 'path', ''));
|
|
if ($path === '') {
|
|
return [];
|
|
}
|
|
|
|
$contentBase64 = (string) ($result['content_base64'] ?? '');
|
|
$content = $contentBase64 !== '' ? base64_decode($contentBase64, true) : false;
|
|
$content = is_string($content) ? $content : '';
|
|
|
|
$file = ServerFile::query()->updateOrCreate(
|
|
[
|
|
'customer_hosting_order_id' => $task->customer_hosting_order_id,
|
|
'path' => $path,
|
|
],
|
|
[
|
|
'server_agent_id' => $task->server_agent_id,
|
|
'latest_server_task_id' => $task->id,
|
|
'kind' => 'file',
|
|
'exists' => true,
|
|
'size_bytes' => strlen($content),
|
|
'content_hash' => $content !== '' ? hash('sha256', $content) : null,
|
|
'content_preview' => $this->previewForContent($content),
|
|
'metadata' => [
|
|
'encoding' => 'base64',
|
|
'server_task_id' => $task->id,
|
|
],
|
|
'last_synced_at' => now(),
|
|
]
|
|
);
|
|
|
|
return [
|
|
'server_file_id' => $file->id,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $result
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function persistFileWriteTask(ServerTask $task, array $result): array
|
|
{
|
|
$path = (string) ($result['path'] ?? data_get($task->payload, 'path', ''));
|
|
if ($path === '') {
|
|
return [];
|
|
}
|
|
|
|
$content = (string) data_get($task->payload, 'content', '');
|
|
$file = ServerFile::query()->updateOrCreate(
|
|
[
|
|
'customer_hosting_order_id' => $task->customer_hosting_order_id,
|
|
'path' => $path,
|
|
],
|
|
[
|
|
'server_agent_id' => $task->server_agent_id,
|
|
'latest_server_task_id' => $task->id,
|
|
'kind' => 'file',
|
|
'exists' => true,
|
|
'size_bytes' => strlen($content),
|
|
'content_hash' => $content !== '' ? hash('sha256', $content) : null,
|
|
'content_preview' => $this->previewForContent($content),
|
|
'metadata' => [
|
|
'server_task_id' => $task->id,
|
|
'write_source' => 'server_agent',
|
|
],
|
|
'last_synced_at' => now(),
|
|
]
|
|
);
|
|
|
|
return [
|
|
'server_file_id' => $file->id,
|
|
];
|
|
}
|
|
|
|
private function normalizeFileKind(string $type): string
|
|
{
|
|
return match ($type) {
|
|
'd' => 'directory',
|
|
'l' => 'symlink',
|
|
default => 'file',
|
|
};
|
|
}
|
|
|
|
private function previewForContent(string $content): ?string
|
|
{
|
|
if ($content === '') {
|
|
return null;
|
|
}
|
|
|
|
if (preg_match('//u', $content) !== 1) {
|
|
return Str::limit(base64_encode(substr($content, 0, 2000)), 4000, '');
|
|
}
|
|
|
|
return (string) Str::of($content)
|
|
->replaceMatches('/[^\P{C}\t\r\n]/u', '')
|
|
->limit(4000, '');
|
|
}
|
|
}
|