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>
203 lines
7.7 KiB
PHP
203 lines
7.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Hosting;
|
|
|
|
use App\Models\CustomerHostingOrder;
|
|
use App\Models\ServerAgent;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class ServerAgentBootstrapService
|
|
{
|
|
/**
|
|
* @return array{agent: ServerAgent, bootstrap_token: string, registration_url: string, heartbeat_url: string}
|
|
*/
|
|
public function ensureBootstrap(CustomerHostingOrder $order): array
|
|
{
|
|
$order->loadMissing('serverAgent');
|
|
|
|
$meta = (array) ($order->meta ?? []);
|
|
$agentMeta = (array) ($meta['server_agent'] ?? []);
|
|
$bootstrapToken = $this->decryptBootstrapToken((string) ($agentMeta['bootstrap_token_encrypted'] ?? ''));
|
|
|
|
if ($bootstrapToken === null) {
|
|
$bootstrapToken = Str::random(64);
|
|
}
|
|
|
|
$agent = $order->serverAgent ?: new ServerAgent([
|
|
'customer_hosting_order_id' => $order->id,
|
|
'uuid' => (string) Str::uuid(),
|
|
'status' => ServerAgent::STATUS_PENDING_REGISTRATION,
|
|
]);
|
|
|
|
$agent->fill([
|
|
'bootstrap_token_hash' => hash('sha256', $bootstrapToken),
|
|
]);
|
|
$agent->save();
|
|
|
|
$agentMeta = array_merge($agentMeta, [
|
|
'agent_id' => $agent->id,
|
|
'agent_uuid' => $agent->uuid,
|
|
'bootstrap_token_encrypted' => encrypt($bootstrapToken),
|
|
'registration_url' => route('api.server-agents.register'),
|
|
'heartbeat_url' => route('api.server-agents.heartbeat'),
|
|
'task_progress_url_template' => url('/api/server-agents/tasks/{task}/progress'),
|
|
'task_complete_url_template' => url('/api/server-agents/tasks/{task}/complete'),
|
|
'task_fail_url_template' => url('/api/server-agents/tasks/{task}/fail'),
|
|
]);
|
|
|
|
$meta['server_agent'] = $agentMeta;
|
|
$meta['server_panel_runtime'] = $this->withRuntimeAgentState(
|
|
(array) ($meta['server_panel_runtime'] ?? []),
|
|
$agent
|
|
);
|
|
|
|
$order->update(['meta' => $meta]);
|
|
|
|
return [
|
|
'agent' => $agent->fresh(),
|
|
'bootstrap_token' => $bootstrapToken,
|
|
'registration_url' => $agentMeta['registration_url'],
|
|
'heartbeat_url' => $agentMeta['heartbeat_url'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $attributes
|
|
* @return array{agent: ServerAgent, access_token: string}
|
|
*/
|
|
public function register(CustomerHostingOrder $order, string $bootstrapToken, array $attributes): array
|
|
{
|
|
$bootstrap = $this->ensureBootstrap($order);
|
|
$agent = $bootstrap['agent'];
|
|
|
|
if (! hash_equals((string) $agent->bootstrap_token_hash, hash('sha256', $bootstrapToken))) {
|
|
throw ValidationException::withMessages([
|
|
'bootstrap_token' => 'The bootstrap token is invalid.',
|
|
]);
|
|
}
|
|
|
|
$accessToken = Str::random(80);
|
|
|
|
$agent->fill([
|
|
'status' => ServerAgent::STATUS_ONLINE,
|
|
'hostname' => (string) ($attributes['hostname'] ?? $agent->hostname),
|
|
'agent_version' => (string) ($attributes['agent_version'] ?? $agent->agent_version),
|
|
'platform' => (string) ($attributes['platform'] ?? $agent->platform),
|
|
'capabilities' => array_values(array_unique((array) ($attributes['capabilities'] ?? []))),
|
|
'metadata' => (array) ($attributes['metadata'] ?? $agent->metadata ?? []),
|
|
'access_token_hash' => hash('sha256', $accessToken),
|
|
'access_token_issued_at' => now(),
|
|
'registered_at' => $agent->registered_at ?? now(),
|
|
'last_heartbeat_at' => now(),
|
|
'last_seen_ip' => (string) ($attributes['last_seen_ip'] ?? $agent->last_seen_ip),
|
|
]);
|
|
$agent->save();
|
|
|
|
$this->syncOrderRuntime($order->fresh(), $agent->fresh());
|
|
|
|
return [
|
|
'agent' => $agent->fresh(),
|
|
'access_token' => $accessToken,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $attributes
|
|
*/
|
|
public function recordHeartbeat(ServerAgent $agent, array $attributes = []): ServerAgent
|
|
{
|
|
$agent->fill([
|
|
'status' => ServerAgent::STATUS_ONLINE,
|
|
'hostname' => (string) ($attributes['hostname'] ?? $agent->hostname),
|
|
'agent_version' => (string) ($attributes['agent_version'] ?? $agent->agent_version),
|
|
'platform' => (string) ($attributes['platform'] ?? $agent->platform),
|
|
'capabilities' => array_values(array_unique((array) ($attributes['capabilities'] ?? $agent->capabilities ?? []))),
|
|
'metadata' => array_replace((array) ($agent->metadata ?? []), (array) ($attributes['metadata'] ?? [])),
|
|
'last_heartbeat_at' => now(),
|
|
'last_seen_ip' => (string) ($attributes['last_seen_ip'] ?? $agent->last_seen_ip),
|
|
]);
|
|
$agent->save();
|
|
|
|
if ($agent->relationLoaded('order')) {
|
|
$this->syncOrderRuntime($agent->order, $agent);
|
|
} else {
|
|
$this->syncOrderRuntime($agent->order()->firstOrFail(), $agent);
|
|
}
|
|
|
|
return $agent->fresh();
|
|
}
|
|
|
|
public function syncOrderRuntime(CustomerHostingOrder $order, ServerAgent $agent): void
|
|
{
|
|
$meta = (array) ($order->meta ?? []);
|
|
$meta['server_agent'] = array_merge((array) ($meta['server_agent'] ?? []), [
|
|
'agent_id' => $agent->id,
|
|
'agent_uuid' => $agent->uuid,
|
|
'status' => $agent->status,
|
|
'hostname' => $agent->hostname,
|
|
'agent_version' => $agent->agent_version,
|
|
'platform' => $agent->platform,
|
|
'capabilities' => (array) ($agent->capabilities ?? []),
|
|
'registered_at' => optional($agent->registered_at)->toIso8601String(),
|
|
'last_heartbeat_at' => optional($agent->last_heartbeat_at)->toIso8601String(),
|
|
]);
|
|
$meta['server_panel_runtime'] = $this->withRuntimeAgentState(
|
|
(array) ($meta['server_panel_runtime'] ?? []),
|
|
$agent
|
|
);
|
|
|
|
$order->update(['meta' => $meta]);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $runtime
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function withRuntimeAgentState(array $runtime, ServerAgent $agent): array
|
|
{
|
|
$current = array_values(array_unique(array_merge(
|
|
(array) ($runtime['current_capabilities'] ?? []),
|
|
['Agent registration', 'Async task dispatch']
|
|
)));
|
|
|
|
if ($agent->status === ServerAgent::STATUS_ONLINE) {
|
|
$current = array_values(array_unique(array_merge($current, [
|
|
'Files',
|
|
'Databases',
|
|
'Domains',
|
|
'SSL',
|
|
'PHP',
|
|
'Cron',
|
|
'Logs',
|
|
'Site management',
|
|
])));
|
|
}
|
|
|
|
$runtime['hosting_panel_ready'] = $agent->registered_at !== null;
|
|
$runtime['future_hosting_panel_status'] = $agent->registered_at !== null ? 'agent_registered' : ($runtime['future_hosting_panel_status'] ?? 'planned');
|
|
$runtime['agent_status'] = $agent->status;
|
|
$runtime['current_capabilities'] = $current;
|
|
$runtime['secondary_message'] = $agent->registered_at !== null
|
|
? 'The server agent is registered. Managed tasks for files, databases, domains, SSL, PHP, cron, logs, and site actions are now queued through Ladill.'
|
|
: ($runtime['secondary_message'] ?? null);
|
|
|
|
return $runtime;
|
|
}
|
|
|
|
private function decryptBootstrapToken(string $encryptedToken): ?string
|
|
{
|
|
if ($encryptedToken === '') {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$token = decrypt($encryptedToken);
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
|
|
return is_string($token) && $token !== '' ? $token : null;
|
|
}
|
|
}
|