Initial Ladill Hosting app with Gitea deploy pipeline.
Deploy Ladill Hosting / deploy (push) Failing after 17s
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>
This commit is contained in:
@@ -0,0 +1,557 @@
|
||||
<?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, '');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user