Files
coolify/app/Livewire/Server/Show.php
T

1482 lines
55 KiB
PHP

<?php
namespace App\Livewire\Server;
use App\Actions\Server\StopSentinel;
use App\Enums\ServerRole;
use App\Events\ServerReachabilityChanged;
use App\Models\CloudProviderToken;
use App\Models\Server;
use App\Models\Team;
use App\Rules\ValidServerIp;
use App\Services\DigitalOceanService;
use App\Services\HetznerService;
use App\Services\HostingerService;
use App\Services\ServerTransfer\ServerTransferClaimer;
use App\Services\VultrService;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Locked;
use Livewire\Component;
class Show extends Component
{
use AuthorizesRequests;
public Server $server;
public string $name;
public ?string $description = null;
public string $ip;
public string $user;
public string $port;
public int $connectionTimeout;
public ?string $validationLogs = null;
public ?string $wildcardDomain = null;
public bool $isReachable;
public bool $isUsable;
#[Locked]
public bool $isSwarmManager;
#[Locked]
public bool $isSwarmWorker;
public string $serverRole;
#[Locked]
public ?string $pendingServerRole = null;
public bool $isMetricsEnabled;
public string $sentinelToken;
public ?string $sentinelUpdatedAt = null;
public int $sentinelMetricsRefreshRateSeconds;
public int $sentinelMetricsHistoryDays;
public int $sentinelPushIntervalSeconds;
public ?string $sentinelCustomUrl = null;
public bool $isSentinelDebugEnabled;
public ?string $sentinelCustomDockerImage = null;
public string $serverTimezone;
public ?string $hetznerServerStatus = null;
public ?string $vultrInstanceStatus = null;
public ?string $digitalOceanDropletStatus = null;
public ?string $hostingerVirtualMachineStatus = null;
public bool $isValidating = false;
// Hetzner linking properties
public Collection $availableHetznerTokens;
public ?int $selectedHetznerTokenId = null;
public ?string $manualHetznerServerId = null;
public ?array $matchedHetznerServer = null;
public ?string $hetznerSearchError = null;
public bool $hetznerNoMatchFound = false;
public Collection $availableVultrTokens;
public ?int $selectedVultrTokenId = null;
public ?string $manualVultrInstanceId = null;
public ?array $matchedVultrInstance = null;
public ?string $vultrSearchError = null;
public bool $vultrNoMatchFound = false;
public Collection $availableDigitalOceanTokens;
public ?int $selectedDigitalOceanTokenId = null;
public ?string $manualDigitalOceanDropletId = null;
public ?array $matchedDigitalOceanDroplet = null;
public ?string $digitalOceanSearchError = null;
public bool $digitalOceanNoMatchFound = false;
public Collection $availableHostingerTokens;
public ?int $selectedHostingerTokenId = null;
public ?string $manualHostingerVirtualMachineId = null;
public ?array $matchedHostingerVirtualMachine = null;
public ?string $hostingerSearchError = null;
public bool $hostingerNoMatchFound = false;
public function getListeners()
{
$teamId = $this->server->team_id ?? auth()->user()->currentTeam()->id;
return [
'refreshServerShow' => 'refresh',
'refreshServer' => '$refresh',
"echo-private:team.{$teamId},SentinelRestarted" => 'handleSentinelRestarted',
"echo-private:team.{$teamId},ServerValidated" => 'handleServerValidated',
];
}
protected function rules(): array
{
return [
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
'ip' => ['required', new ValidServerIp],
'user' => ValidationPatterns::serverUsernameRules(),
'port' => 'required|integer|between:1,65535',
'connectionTimeout' => 'required|integer|min:1|max:300',
'validationLogs' => 'nullable',
'wildcardDomain' => 'nullable|url',
'isReachable' => 'required',
'isUsable' => 'required',
'isSwarmManager' => 'required',
'isSwarmWorker' => 'required',
'serverRole' => ['required', 'in:deployment,build,both'],
'isMetricsEnabled' => 'required',
'sentinelToken' => 'required',
'sentinelUpdatedAt' => 'nullable',
'sentinelMetricsRefreshRateSeconds' => 'required|integer|min:1',
'sentinelMetricsHistoryDays' => 'required|integer|min:1',
'sentinelPushIntervalSeconds' => 'required|integer|min:10',
'sentinelCustomUrl' => 'nullable|url',
'isSentinelDebugEnabled' => 'required',
'serverTimezone' => 'required',
];
}
protected function messages(): array
{
return array_merge(
ValidationPatterns::combinedMessages(),
[
'ip.required' => 'The IP Address field is required.',
'user.required' => 'The User field is required.',
...ValidationPatterns::serverUsernameMessages(),
'port.required' => 'The Port field is required.',
'connectionTimeout.required' => 'The SSH Connection Timeout field is required.',
'connectionTimeout.integer' => 'The SSH Connection Timeout must be an integer.',
'connectionTimeout.min' => 'The SSH Connection Timeout must be at least 1 second.',
'connectionTimeout.max' => 'The SSH Connection Timeout must not exceed 300 seconds.',
'wildcardDomain.url' => 'The Wildcard Domain must be a valid URL.',
'sentinelToken.required' => 'The Sentinel Token field is required.',
'sentinelMetricsRefreshRateSeconds.required' => 'The Metrics Refresh Rate field is required.',
'sentinelMetricsRefreshRateSeconds.integer' => 'The Metrics Refresh Rate must be an integer.',
'sentinelMetricsRefreshRateSeconds.min' => 'The Metrics Refresh Rate must be at least 1 second.',
'sentinelMetricsHistoryDays.required' => 'The Metrics History Days field is required.',
'sentinelMetricsHistoryDays.integer' => 'The Metrics History Days must be an integer.',
'sentinelMetricsHistoryDays.min' => 'The Metrics History Days must be at least 1 day.',
'sentinelPushIntervalSeconds.required' => 'The Push Interval field is required.',
'sentinelPushIntervalSeconds.integer' => 'The Push Interval must be an integer.',
'sentinelPushIntervalSeconds.min' => 'The Push Interval must be at least 10 seconds.',
'sentinelCustomUrl.url' => 'The Custom Sentinel URL must be a valid URL.',
'serverTimezone.required' => 'The Server Timezone field is required.',
]
);
}
public function mount(string $server_uuid)
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
$this->syncData();
// Load saved Hetzner status and validation state
$this->hetznerServerStatus = $this->server->hetzner_server_status;
$this->vultrInstanceStatus = $this->server->vultr_instance_status;
$this->digitalOceanDropletStatus = $this->server->digitalocean_droplet_status;
$this->hostingerVirtualMachineStatus = $this->server->hostinger_virtual_machine_status;
$this->isValidating = $this->server->is_validating ?? false;
// Load cloud provider tokens for linking
$this->loadHetznerTokens();
$this->loadVultrTokens();
$this->loadDigitalOceanTokens();
$this->loadHostingerTokens();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
#[Computed]
public function timezones(): array
{
return collect(timezone_identifiers_list())
->sort()
->values()
->toArray();
}
private function syncData(bool $toModel = false): void
{
if ($toModel) {
$this->validate();
$this->ip = Server::normalizeIp($this->ip);
if ($this->server->isTransferredAway() && $this->connectionFieldsChanged()) {
$this->ip = $this->server->ip;
$this->user = $this->server->user;
$this->port = $this->server->port;
throw new \Exception('The IP address, SSH user, and SSH port of a transferred server cannot be changed.');
}
// Check only a changed IP, so that an old duplicate does not block saving other fields.
$ipChanged = $this->ip !== Server::normalizeIp($this->server->getRawOriginal('ip'));
$foundServer = $ipChanged ? Server::where('ip', $this->ip)
->where('id', '!=', $this->server->id)
->first() : null;
if ($foundServer) {
$this->ip = $this->server->ip;
throw new \Exception('A server with this IP/Domain already exists.');
}
$this->server->name = $this->name;
$this->server->description = $this->description;
$this->server->ip = $this->ip;
$this->server->user = $this->user;
$this->server->port = $this->port;
$this->server->validation_logs = $this->validationLogs;
$this->server->save();
$this->server->settings->connection_timeout = $this->connectionTimeout;
$this->server->settings->wildcard_domain = $this->wildcardDomain;
// The role is only changed through requestServerRoleChange()/confirmServerRoleChange().
$this->serverRole = $this->server->settings->effectiveServerRole()->value;
$this->server->settings->is_metrics_enabled = $this->isMetricsEnabled;
$this->server->settings->sentinel_token = $this->sentinelToken;
$this->server->settings->sentinel_metrics_refresh_rate_seconds = $this->sentinelMetricsRefreshRateSeconds;
$this->server->settings->sentinel_metrics_history_days = $this->sentinelMetricsHistoryDays;
$this->server->settings->sentinel_push_interval_seconds = $this->sentinelPushIntervalSeconds;
$this->server->settings->sentinel_custom_url = $this->sentinelCustomUrl;
$this->server->settings->is_sentinel_debug_enabled = $this->isSentinelDebugEnabled;
if (! validate_timezone($this->serverTimezone)) {
$this->serverTimezone = config('app.timezone');
throw new \Exception('Invalid timezone.');
} else {
$this->server->settings->server_timezone = $this->serverTimezone;
}
$changedFields = auditChangedFields($this->server->settings);
$this->server->settings->save();
$this->auditSettingsUpdate($changedFields);
} else {
$this->name = $this->server->name;
$this->description = $this->server->description;
$this->ip = $this->server->ip;
$this->user = $this->server->user;
$this->port = $this->server->port;
$this->connectionTimeout = $this->server->settings->connection_timeout;
$this->wildcardDomain = $this->server->settings->wildcard_domain;
$this->isReachable = $this->server->settings->is_reachable;
$this->isUsable = $this->server->settings->is_usable;
$this->isSwarmManager = $this->server->settings->is_swarm_manager;
$this->isSwarmWorker = $this->server->settings->is_swarm_worker;
$this->serverRole = $this->server->settings->effectiveServerRole()->value;
$this->isMetricsEnabled = $this->server->settings->is_metrics_enabled;
$this->sentinelToken = auth()->user()->can('update', $this->server)
? $this->server->settings->ensureValidSentinelToken()
: '';
$this->sentinelMetricsRefreshRateSeconds = $this->server->settings->sentinel_metrics_refresh_rate_seconds;
$this->sentinelMetricsHistoryDays = $this->server->settings->sentinel_metrics_history_days;
$this->sentinelPushIntervalSeconds = $this->server->settings->sentinel_push_interval_seconds;
$this->sentinelCustomUrl = auth()->user()->can('update', $this->server)
? $this->server->settings->sentinel_custom_url
: null;
$this->isSentinelDebugEnabled = $this->server->settings->is_sentinel_debug_enabled;
$this->sentinelUpdatedAt = $this->server->sentinel_updated_at;
$this->serverTimezone = $this->server->settings->server_timezone;
$this->isValidating = $this->server->is_validating ?? false;
}
}
private function connectionFieldsChanged(): bool
{
return $this->ip !== Server::normalizeIp($this->server->getRawOriginal('ip'))
|| (string) $this->user !== (string) $this->server->user
|| (int) $this->port !== (int) $this->server->port;
}
/**
* Server columns are audited by the model. Settings live on ServerSetting, so record their names here.
*
* @param array<int, string> $changedFields
*/
private function auditSettingsUpdate(array $changedFields): void
{
if ($changedFields === []) {
return;
}
auditLog('ui.server.settings_updated', [
'team_id' => $this->server->team_id,
'server_uuid' => $this->server->uuid,
'server_name' => $this->server->name,
'changed_fields' => $changedFields,
]);
}
public function refresh()
{
$this->syncData();
}
public function handleSentinelRestarted($event)
{
// Only refresh if the event is for this server
if (isset($event['serverUuid']) && $event['serverUuid'] === $this->server->uuid) {
$this->server->refresh();
// Only refresh display-only state; never re-sync text-input properties
// (would clobber any unsaved typing — see coolify#6062 / #6354 / #9695).
$this->sentinelUpdatedAt = $this->server->sentinel_updated_at;
$this->dispatch('success', 'Sentinel has been restarted successfully.');
}
}
public function validateServer($install = true)
{
try {
$this->authorize('update', $this->server);
if (! $this->server->canBeValidated()) {
$this->dispatch(
'error',
'Cannot revalidate',
'This server was transferred to another Coolify instance. Manage it from the target instance instead.'
);
return;
}
if ($this->server->vultr_instance_id) {
$status = $this->server->refreshVultrState();
$this->server->refresh();
$this->vultrInstanceStatus = $this->server->vultr_instance_status;
$this->ip = $this->server->ip;
if (in_array($status, ['stopped', 'suspended', 'deleted'], true)) {
$message = $status === 'deleted'
? 'Vultr instance is deleted or no longer accessible. Relink this server before validating.'
: 'Vultr instance is '.($status ?? 'not running').'. Power it on before validating.';
$this->dispatch('error', $message);
return;
}
}
$this->validationLogs = $this->server->validation_logs = null;
$this->server->save();
$this->dispatch('init', $install);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
/**
* Take back a server that was transferred to another instance.
*/
public function enableManagement(ServerTransferClaimer $claimer): void
{
$this->authorize('update', $this->server);
if ($this->server->isLocalhost() || ! $this->server->isTransferredAway()) {
return;
}
// The archive does not use a server slot, so taking it back needs a free slot.
if (Team::serverLimitReached($this->server->team)) {
$this->dispatch('error', 'Your team is over its server limit. Upgrade your subscription or remove a server first.');
return;
}
try {
// The claim connects to the host over SSH, so it uses the same Cloud limit as a validation.
Server::throttleValidation($this->server->team_id, $this->server->ip);
$claimer->claim($this->server);
} catch (\Throwable $e) {
handleError($e, $this);
return;
}
$claimer->restoreAfterTakeBack($this->server->refresh());
$this->server->refresh();
// Sentinel must report here. While it reports to the other instance, that instance does not run
// the connection check that reads the owner marker and makes it stop managing the server.
$this->server->restartSentinel();
$this->syncData();
$this->dispatch('transferFinishStarted')->to(TransferFinish::class);
auditLog('ui.server.management_enabled', [
'team_id' => $this->server->team_id,
'server_uuid' => $this->server->uuid,
'server_name' => $this->server->name,
]);
$this->dispatch('success', 'This Coolify instance now manages the server.');
}
public function checkLocalhostConnection()
{
try {
$this->authorize('update', $this->server);
$this->syncData(true);
Server::throttleValidation($this->server->team_id, $this->server->ip);
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
if ($uptime) {
$this->dispatch('success', 'Server is reachable.');
$this->server->settings->is_reachable = $this->isReachable = true;
$this->server->settings->is_usable = $this->isUsable = true;
$this->server->settings->save();
ServerReachabilityChanged::dispatch($this->server);
$this->server->gatherServerMetadata();
$this->server->refresh();
} else {
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br>Error: '.$error);
return;
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function restartSentinel()
{
try {
$this->authorize('manageSentinel', $this->server);
$customImage = isDev() ? $this->sentinelCustomDockerImage : null;
$this->server->restartSentinel($customImage);
$this->dispatch('info', 'Restarting Sentinel.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function updatedIsSentinelDebugEnabled($value)
{
try {
// Saving the setting restarts Sentinel (ServerSetting::booted()).
$this->submit();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function updatedIsMetricsEnabled($value)
{
try {
// Saving the setting restarts Sentinel (ServerSetting::booted()).
$this->submit();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function requestServerRoleChange(): void
{
try {
$this->authorize('update', $this->server);
$newRole = ServerRole::from($this->serverRole);
$currentRole = $this->server->settings()->firstOrFail()->effectiveServerRole();
if ($newRole === ServerRole::DEPLOYMENT && $this->server->hasEnabledGithubRunners()) {
$this->serverRole = $currentRole->value;
$this->dispatch('error', 'Disable the GitHub runners before you set this server to deployments only.');
return;
}
if ($newRole === ServerRole::BUILD && ! $this->server->isEmpty()) {
$this->serverRole = $currentRole->value;
$this->dispatch('error', 'Move or remove the existing resources before you set this server to build only.');
return;
}
if ($newRole === ServerRole::DEPLOYMENT && ! Server::buildServers($this->server->team_id)->whereKeyNot($this->server->id)->exists()) {
$this->serverRole = $currentRole->value;
$this->dispatch('error', 'Add a usable build server before you set this server to deployments only.');
return;
}
if ($newRole === ServerRole::BOTH && $currentRole !== ServerRole::BOTH) {
$this->pendingServerRole = $newRole->value;
$this->serverRole = $currentRole->value;
$this->dispatch('open-server-role-confirmation');
return;
}
$this->saveServerRole($newRole);
} catch (\Throwable $e) {
handleError($e, $this);
}
}
public function confirmServerRoleChange(): void
{
try {
$this->authorize('update', $this->server);
$role = ServerRole::from($this->pendingServerRole ?? '');
$this->pendingServerRole = null;
$this->saveServerRole($role);
} catch (\Throwable $e) {
handleError($e, $this);
}
}
private function saveServerRole(ServerRole $role): void
{
$this->serverRole = $role->value;
$this->server->settings->server_role = $role;
$this->server->settings->is_build_server = $role === ServerRole::BUILD;
if ($role === ServerRole::BUILD && $this->server->isSentinelEnabled()) {
$this->isMetricsEnabled = false;
$this->isSentinelDebugEnabled = false;
$this->server->settings->is_sentinel_enabled = false;
StopSentinel::dispatch($this->server);
$this->dispatch('info', 'Sentinel has been disabled as build servers cannot run Sentinel.');
}
$this->submit();
$this->dispatch('refreshServerShow');
}
public function regenerateSentinelToken()
{
try {
$this->authorize('manageSentinel', $this->server);
$this->server->settings->generateSentinelToken();
auditLog('ui.server.sentinel.token_regenerated', [
'team_id' => $this->server->team_id,
'server_uuid' => $this->server->uuid,
'server_name' => $this->server->name,
]);
$this->dispatch('success', 'Token regenerated. Restarting Sentinel.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
$this->authorize('update', $this->server);
$this->syncData(true);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkHetznerServerStatus(bool $manual = false)
{
try {
$this->authorize('view', $this->server);
if ($this->server->isTransferredAway()) {
return;
}
if (! $this->server->hetzner_server_id || ! $this->server->cloudProviderToken) {
$this->dispatch('error', 'This server is not associated with a Hetzner Cloud server or token.');
return;
}
$hetznerService = new HetznerService($this->server->cloudProviderToken->token);
$serverData = $hetznerService->getServer($this->server->hetzner_server_id);
$this->hetznerServerStatus = $serverData['status'] ?? null;
// Save status to database without triggering model events
if ($this->server->hetzner_server_status !== $this->hetznerServerStatus) {
$this->server->hetzner_server_status = $this->hetznerServerStatus;
$this->server->update(['hetzner_server_status' => $this->hetznerServerStatus]);
}
$assignedIp = data_get($serverData, 'public_net.ipv4.ip') ?? hetznerServerIpv6(data_get($serverData, 'public_net.ipv6.ip'));
if ($this->server->backfillPlaceholderIp($assignedIp)) {
$this->ip = $this->server->ip;
}
if ($manual) {
$this->dispatch('success', 'Server status refreshed: '.ucfirst($this->hetznerServerStatus ?? 'unknown'));
}
// If Hetzner server is off but Coolify thinks it's still reachable, update Coolify's state
if ($this->hetznerServerStatus === 'off' && $this->server->settings->is_reachable) {
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
if ($uptime) {
$this->dispatch('success', 'Server is reachable.');
$this->server->settings->is_reachable = $this->isReachable = true;
$this->server->settings->is_usable = $this->isUsable = true;
$this->server->settings->save();
ServerReachabilityChanged::dispatch($this->server);
} else {
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br>Error: '.$error);
return;
}
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkVultrInstanceStatus(bool $manual = false)
{
try {
$this->authorize('view', $this->server);
if ($this->server->isTransferredAway()) {
return;
}
if (! $this->server->vultr_instance_id || ! $this->server->cloudProviderToken) {
$this->dispatch('error', 'This server is not associated with a Vultr instance or token.');
return;
}
$this->vultrInstanceStatus = $this->server->refreshVultrState();
$this->server->refresh();
$this->ip = $this->server->ip;
if ($manual) {
$this->dispatch('success', 'Instance status refreshed: '.ucfirst($this->vultrInstanceStatus ?? 'unknown'));
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkDigitalOceanDropletStatus(bool $manual = false)
{
try {
$this->authorize('view', $this->server);
if ($this->server->isTransferredAway()) {
return;
}
if (! $this->server->digitalocean_droplet_id || ! $this->server->cloudProviderToken) {
$this->dispatch('error', 'This server is not associated with a DigitalOcean droplet or token.');
return;
}
$this->digitalOceanDropletStatus = $this->server->refreshDigitalOceanState();
$this->server->refresh();
$this->ip = $this->server->ip;
if ($manual) {
$this->dispatch('success', 'Droplet status refreshed: '.ucfirst($this->digitalOceanDropletStatus ?? 'unknown'));
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkHostingerVirtualMachineStatus(bool $manual = false): mixed
{
try {
$this->authorize('view', $this->server);
if ($this->server->isTransferredAway()) {
return null;
}
if (! $this->server->hostinger_virtual_machine_id || ! $this->server->cloudProviderToken) {
$this->dispatch('error', 'This server is not associated with a Hostinger VPS or token.');
return null;
}
$this->hostingerVirtualMachineStatus = $this->server->refreshHostingerState();
$this->server->refresh();
$this->ip = $this->server->ip;
if ($manual) {
$this->dispatch('success', 'VPS status refreshed: '.ucfirst($this->hostingerVirtualMachineStatus ?? 'unknown'));
}
return null;
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function handleServerValidated($event = null)
{
// Check if event is for this server
if ($event && isset($event['serverUuid']) && $event['serverUuid'] !== $this->server->uuid) {
return;
}
// Refresh server data and only the display-only state that validation produces.
// Never re-sync text-input properties via syncData() — would clobber any
// unsaved typing (see coolify#6062 / #6354 / #9695).
$this->server->refresh();
$this->server->settings->refresh();
$this->isValidating = $this->server->is_validating ?? false;
$this->validationLogs = $this->server->validation_logs;
$this->isReachable = $this->server->settings->is_reachable;
$this->isUsable = $this->server->settings->is_usable;
// Reload Hetzner tokens in case the linking section should now be shown
$this->loadHetznerTokens();
$this->loadVultrTokens();
$this->loadDigitalOceanTokens();
$this->loadHostingerTokens();
$this->dispatch('refreshServerShow');
$this->dispatch('refreshServer');
}
public function startHetznerServer()
{
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
if (! $this->server->hetzner_server_id || ! $this->server->cloudProviderToken) {
$this->dispatch('error', 'This server is not associated with a Hetzner Cloud server or token.');
return;
}
$hetznerService = new HetznerService($this->server->cloudProviderToken->token);
$hetznerService->powerOnServer($this->server->hetzner_server_id);
$this->hetznerServerStatus = 'starting';
$this->server->update(['hetzner_server_status' => 'starting']);
$this->dispatch('success', 'Hetzner server is starting...');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function startVultrInstance()
{
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
if (! $this->server->vultr_instance_id || ! $this->server->cloudProviderToken) {
$this->dispatch('error', 'This server is not associated with a Vultr instance or token.');
return;
}
$vultrService = new VultrService($this->server->cloudProviderToken->token);
$vultrService->startInstance($this->server->vultr_instance_id);
$this->vultrInstanceStatus = 'starting';
$this->server->update(['vultr_instance_status' => 'starting']);
$this->dispatch('success', 'Vultr instance is starting...');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function startDigitalOceanDroplet()
{
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
if (! $this->server->digitalocean_droplet_id || ! $this->server->cloudProviderToken) {
$this->dispatch('error', 'This server is not associated with a DigitalOcean droplet or token.');
return;
}
$digitalOceanService = new DigitalOceanService($this->server->cloudProviderToken->token);
$digitalOceanService->powerOnDroplet((int) $this->server->digitalocean_droplet_id);
$this->digitalOceanDropletStatus = 'new';
$this->server->update(['digitalocean_droplet_status' => 'new']);
$this->dispatch('success', 'DigitalOcean droplet is starting...');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function startHostingerVirtualMachine(): mixed
{
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
if (! $this->server->hostinger_virtual_machine_id || ! $this->server->cloudProviderToken) {
$this->dispatch('error', 'This server is not associated with a Hostinger VPS or token.');
return null;
}
$hostingerService = new HostingerService($this->server->cloudProviderToken->token);
$hostingerService->startVirtualMachine((int) $this->server->hostinger_virtual_machine_id);
$this->hostingerVirtualMachineStatus = 'starting';
$this->server->update(['hostinger_virtual_machine_status' => 'starting']);
$this->dispatch('success', 'Hostinger VPS is starting...');
return null;
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function refreshServerMetadata(): void
{
try {
$this->authorize('update', $this->server);
$providerRefreshed = $this->refreshProviderStatus();
if (! $this->server->isFunctional()) {
$providerRefreshed
? $this->dispatch('info', 'Provider status refreshed. Validate the server connection to fetch server details.')
: $this->dispatch('error', 'Validate the server connection before fetching details.');
return;
}
$result = $this->server->gatherServerMetadata();
if ($result) {
$this->server->refresh()->load('settings');
$this->dispatch('success', 'Server details refreshed.');
} else {
$this->dispatch('error', 'Could not collect server details. Check the application logs for the remote command output.');
}
} catch (\Throwable $e) {
handleError($e, $this);
}
}
/**
* Refresh the state of the linked cloud provider server, if there is one.
*/
private function refreshProviderStatus(): bool
{
if (! $this->server->cloudProviderToken || $this->server->isTransferredAway()) {
return false;
}
$check = match (true) {
filled($this->server->hetzner_server_id) => $this->checkHetznerServerStatus(...),
filled($this->server->digitalocean_droplet_id) => $this->checkDigitalOceanDropletStatus(...),
filled($this->server->vultr_instance_id) => $this->checkVultrInstanceStatus(...),
filled($this->server->hostinger_virtual_machine_id) => $this->checkHostingerVirtualMachineStatus(...),
default => null,
};
if (! $check) {
return false;
}
$check();
return true;
}
public function submit()
{
try {
$this->authorize('update', $this->server);
$this->syncData(true);
$this->dispatch('success', 'Server settings updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function loadHetznerTokens(): void
{
$this->availableHetznerTokens = CloudProviderToken::where('team_id', $this->server->team_id)
->where('provider', 'hetzner')
->get();
}
public function loadVultrTokens(): void
{
$this->availableVultrTokens = CloudProviderToken::where('team_id', $this->server->team_id)
->where('provider', 'vultr')
->get();
}
public function loadDigitalOceanTokens(): void
{
$this->availableDigitalOceanTokens = CloudProviderToken::where('team_id', $this->server->team_id)
->where('provider', 'digitalocean')
->get();
}
public function loadHostingerTokens(): void
{
$this->availableHostingerTokens = CloudProviderToken::ownedByCurrentTeam()
->where('provider', 'hostinger')
->get();
}
#[Computed]
public function limaStartCommand(): ?string
{
if (! isDev()) {
return null;
}
return match ($this->server->uuid) {
'lima-ubuntu-2404' => 'limactl start --yes --name=coolify-lima-ubuntu-2404 docker/lima/ubuntu-2404.yaml',
'lima-ubuntu-2604' => 'limactl start --yes --name=coolify-lima-ubuntu-2604 docker/lima/ubuntu-2604.yaml',
default => null,
};
}
public function searchHetznerServer(): void
{
$this->hetznerSearchError = null;
$this->hetznerNoMatchFound = false;
$this->matchedHetznerServer = null;
if (! $this->selectedHetznerTokenId) {
$this->hetznerSearchError = 'Please select a Hetzner token.';
return;
}
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
$token = $this->availableHetznerTokens->firstWhere('id', $this->selectedHetznerTokenId);
if (! $token) {
$this->hetznerSearchError = 'Invalid token selected.';
return;
}
$hetznerService = new HetznerService($token->token);
$matched = $hetznerService->findServerByIp($this->server->ip);
if ($matched) {
$this->matchedHetznerServer = $matched;
} else {
$this->hetznerNoMatchFound = true;
}
} catch (\Throwable $e) {
$this->hetznerSearchError = 'Failed to search Hetzner servers: '.$e->getMessage();
}
}
public function searchHetznerServerById(): void
{
$this->hetznerSearchError = null;
$this->hetznerNoMatchFound = false;
$this->matchedHetznerServer = null;
if (! $this->selectedHetznerTokenId) {
$this->hetznerSearchError = 'Please select a Hetzner token first.';
return;
}
if (! $this->manualHetznerServerId) {
$this->hetznerSearchError = 'Please enter a Hetzner Server ID.';
return;
}
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
$token = $this->availableHetznerTokens->firstWhere('id', $this->selectedHetznerTokenId);
if (! $token) {
$this->hetznerSearchError = 'Invalid token selected.';
return;
}
$hetznerService = new HetznerService($token->token);
$serverData = $hetznerService->getServer((int) $this->manualHetznerServerId);
if (! empty($serverData)) {
$this->matchedHetznerServer = $serverData;
} else {
$this->hetznerNoMatchFound = true;
}
} catch (\Throwable $e) {
$this->hetznerSearchError = 'Failed to fetch Hetzner server: '.$e->getMessage();
}
}
public function linkToHetzner()
{
if (! $this->matchedHetznerServer) {
$this->dispatch('error', 'No Hetzner server selected.');
return;
}
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
$token = $this->availableHetznerTokens->firstWhere('id', $this->selectedHetznerTokenId);
if (! $token) {
$this->dispatch('error', 'Invalid token selected.');
return;
}
// Verify the server exists and is accessible with the token
$hetznerService = new HetznerService($token->token);
$serverData = $hetznerService->getServer($this->matchedHetznerServer['id']);
if (empty($serverData)) {
$this->dispatch('error', 'Could not find Hetzner server with ID: '.$this->matchedHetznerServer['id']);
return;
}
// Update the server with Hetzner details
$this->server->update([
'cloud_provider_token_id' => $this->selectedHetznerTokenId,
'hetzner_server_id' => $this->matchedHetznerServer['id'],
'hetzner_server_status' => $serverData['status'] ?? null,
]);
$this->hetznerServerStatus = $serverData['status'] ?? null;
// Clear the linking state
$this->matchedHetznerServer = null;
$this->selectedHetznerTokenId = null;
$this->manualHetznerServerId = null;
$this->hetznerNoMatchFound = false;
$this->hetznerSearchError = null;
$this->dispatch('success', 'Server successfully linked to Hetzner Cloud!');
$this->dispatch('close-modal');
$this->dispatch('refreshServerShow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function searchDigitalOceanDroplet(): void
{
$this->digitalOceanSearchError = null;
$this->digitalOceanNoMatchFound = false;
$this->matchedDigitalOceanDroplet = null;
if (! $this->selectedDigitalOceanTokenId) {
$this->digitalOceanSearchError = 'Please select a DigitalOcean token.';
return;
}
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
$token = $this->availableDigitalOceanTokens->firstWhere('id', $this->selectedDigitalOceanTokenId);
if (! $token) {
$this->digitalOceanSearchError = 'Invalid token selected.';
return;
}
$digitalOceanService = new DigitalOceanService($token->token);
$matched = $digitalOceanService->findDropletByIp($this->server->ip);
if ($matched) {
$this->matchedDigitalOceanDroplet = $matched;
} else {
$this->digitalOceanNoMatchFound = true;
}
} catch (\Throwable $e) {
$this->digitalOceanSearchError = 'Failed to search DigitalOcean droplets: '.$e->getMessage();
}
}
public function searchDigitalOceanDropletById(): void
{
$this->digitalOceanSearchError = null;
$this->digitalOceanNoMatchFound = false;
$this->matchedDigitalOceanDroplet = null;
if (! $this->selectedDigitalOceanTokenId) {
$this->digitalOceanSearchError = 'Please select a DigitalOcean token first.';
return;
}
if (! $this->manualDigitalOceanDropletId) {
$this->digitalOceanSearchError = 'Please enter a DigitalOcean Droplet ID.';
return;
}
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
$token = $this->availableDigitalOceanTokens->firstWhere('id', $this->selectedDigitalOceanTokenId);
if (! $token) {
$this->digitalOceanSearchError = 'Invalid token selected.';
return;
}
$digitalOceanService = new DigitalOceanService($token->token);
$dropletData = $digitalOceanService->getDroplet((int) $this->manualDigitalOceanDropletId);
if (! empty($dropletData)) {
$this->matchedDigitalOceanDroplet = $dropletData;
} else {
$this->digitalOceanNoMatchFound = true;
}
} catch (\Throwable $e) {
$this->digitalOceanSearchError = 'Failed to fetch DigitalOcean droplet: '.$e->getMessage();
}
}
public function linkToDigitalOcean()
{
if (! $this->matchedDigitalOceanDroplet) {
$this->dispatch('error', 'No DigitalOcean droplet selected.');
return;
}
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
$token = $this->availableDigitalOceanTokens->firstWhere('id', $this->selectedDigitalOceanTokenId);
if (! $token) {
$this->dispatch('error', 'Invalid token selected.');
return;
}
$digitalOceanService = new DigitalOceanService($token->token);
$dropletData = $digitalOceanService->getDroplet((int) $this->matchedDigitalOceanDroplet['id']);
if (empty($dropletData)) {
$this->dispatch('error', 'Could not find DigitalOcean droplet with ID: '.$this->matchedDigitalOceanDroplet['id']);
return;
}
$ip = $digitalOceanService->getPublicIpAddress($dropletData);
$updates = [
'cloud_provider_token_id' => $this->selectedDigitalOceanTokenId,
'digitalocean_droplet_id' => $this->matchedDigitalOceanDroplet['id'],
'digitalocean_droplet_status' => $dropletData['status'] ?? null,
];
if ($ip) {
$updates['ip'] = $ip;
}
$this->server->update($updates);
$this->digitalOceanDropletStatus = $dropletData['status'] ?? null;
$this->matchedDigitalOceanDroplet = null;
$this->selectedDigitalOceanTokenId = null;
$this->manualDigitalOceanDropletId = null;
$this->digitalOceanNoMatchFound = false;
$this->digitalOceanSearchError = null;
$this->dispatch('success', 'Server successfully linked to DigitalOcean!');
$this->dispatch('close-modal');
$this->dispatch('refreshServerShow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function searchHostingerVirtualMachine(): void
{
$this->hostingerSearchError = null;
$this->hostingerNoMatchFound = false;
$this->matchedHostingerVirtualMachine = null;
if (! $this->selectedHostingerTokenId) {
$this->hostingerSearchError = 'Please select a Hostinger token.';
return;
}
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
$token = $this->availableHostingerTokens->firstWhere('id', $this->selectedHostingerTokenId);
if (! $token) {
$this->hostingerSearchError = 'Invalid token selected.';
return;
}
$matched = (new HostingerService($token->token))->findVirtualMachineByIp($this->server->ip);
if ($matched) {
$this->matchedHostingerVirtualMachine = $matched;
} else {
$this->hostingerNoMatchFound = true;
}
} catch (\Throwable $e) {
$this->hostingerSearchError = 'Failed to search Hostinger virtual machines: '.$e->getMessage();
}
}
public function searchHostingerVirtualMachineById(): void
{
$this->hostingerSearchError = null;
$this->hostingerNoMatchFound = false;
$this->matchedHostingerVirtualMachine = null;
if (! $this->selectedHostingerTokenId) {
$this->hostingerSearchError = 'Please select a Hostinger token first.';
return;
}
if (! $this->manualHostingerVirtualMachineId) {
$this->hostingerSearchError = 'Please enter a Hostinger Virtual Machine ID.';
return;
}
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
$token = $this->availableHostingerTokens->firstWhere('id', $this->selectedHostingerTokenId);
if (! $token) {
$this->hostingerSearchError = 'Invalid token selected.';
return;
}
$virtualMachine = (new HostingerService($token->token))
->getVirtualMachine((int) $this->manualHostingerVirtualMachineId);
if ($virtualMachine) {
$this->matchedHostingerVirtualMachine = $virtualMachine;
} else {
$this->hostingerNoMatchFound = true;
}
} catch (\Throwable $e) {
$this->hostingerSearchError = 'Failed to fetch Hostinger virtual machine: '.$e->getMessage();
}
}
public function linkToHostinger(): mixed
{
if (! $this->matchedHostingerVirtualMachine) {
$this->dispatch('error', 'No Hostinger virtual machine selected.');
return null;
}
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
$token = $this->availableHostingerTokens->firstWhere('id', $this->selectedHostingerTokenId);
if (! $token) {
$this->dispatch('error', 'Invalid token selected.');
return null;
}
$hostingerService = new HostingerService($token->token);
$virtualMachine = $hostingerService->getVirtualMachine((int) $this->matchedHostingerVirtualMachine['id']);
if (! $virtualMachine) {
$this->dispatch('error', 'Could not find Hostinger virtual machine with ID: '.$this->matchedHostingerVirtualMachine['id']);
return null;
}
$updates = [
'cloud_provider_token_id' => $this->selectedHostingerTokenId,
'hostinger_virtual_machine_id' => $virtualMachine['id'],
'hostinger_virtual_machine_status' => $virtualMachine['state'] ?? null,
];
$ip = $hostingerService->getPublicIpAddress($virtualMachine);
if ($ip) {
$updates['ip'] = $ip;
}
$this->server->update($updates);
$this->hostingerVirtualMachineStatus = $virtualMachine['state'] ?? null;
$this->matchedHostingerVirtualMachine = null;
$this->selectedHostingerTokenId = null;
$this->manualHostingerVirtualMachineId = null;
$this->hostingerNoMatchFound = false;
$this->hostingerSearchError = null;
$this->dispatch('success', 'Server successfully linked to Hostinger!');
$this->dispatch('close-modal');
$this->dispatch('refreshServerShow');
return null;
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function searchVultrInstance(): void
{
$this->vultrSearchError = null;
$this->vultrNoMatchFound = false;
$this->matchedVultrInstance = null;
if (! $this->selectedVultrTokenId) {
$this->vultrSearchError = 'Please select a Vultr token.';
return;
}
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
$token = $this->availableVultrTokens->firstWhere('id', $this->selectedVultrTokenId);
if (! $token) {
$this->vultrSearchError = 'Invalid token selected.';
return;
}
$vultrService = new VultrService($token->token);
$matched = $vultrService->findInstanceByIp($this->server->ip);
if ($matched) {
$this->matchedVultrInstance = $matched;
} else {
$this->vultrNoMatchFound = true;
}
} catch (\Throwable $e) {
$this->vultrSearchError = 'Failed to search Vultr instances: '.$e->getMessage();
}
}
public function searchVultrInstanceById(): void
{
$this->vultrSearchError = null;
$this->vultrNoMatchFound = false;
$this->matchedVultrInstance = null;
if (! $this->selectedVultrTokenId) {
$this->vultrSearchError = 'Please select a Vultr token first.';
return;
}
if (! $this->manualVultrInstanceId) {
$this->vultrSearchError = 'Please enter a Vultr Instance ID.';
return;
}
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
$token = $this->availableVultrTokens->firstWhere('id', $this->selectedVultrTokenId);
if (! $token) {
$this->vultrSearchError = 'Invalid token selected.';
return;
}
$vultrService = new VultrService($token->token);
$instanceData = $vultrService->getInstance($this->manualVultrInstanceId);
if (! empty($instanceData)) {
$this->matchedVultrInstance = $instanceData;
} else {
$this->vultrNoMatchFound = true;
}
} catch (\Throwable $e) {
$this->vultrSearchError = 'Failed to fetch Vultr instance: '.$e->getMessage();
}
}
public function linkToVultr()
{
if (! $this->matchedVultrInstance) {
$this->dispatch('error', 'No Vultr instance selected.');
return;
}
try {
$this->authorize('update', $this->server);
$this->server->assertNotTransferredAway();
$token = $this->availableVultrTokens->firstWhere('id', $this->selectedVultrTokenId);
if (! $token) {
$this->dispatch('error', 'Invalid token selected.');
return;
}
$vultrService = new VultrService($token->token);
$instanceData = $vultrService->getInstance($this->matchedVultrInstance['id']);
if (empty($instanceData)) {
$this->dispatch('error', 'Could not find Vultr instance with ID: '.$this->matchedVultrInstance['id']);
return;
}
$this->server->update([
'cloud_provider_token_id' => $this->selectedVultrTokenId,
'vultr_instance_id' => $this->matchedVultrInstance['id'],
'vultr_instance_status' => $instanceData['status'] ?? null,
]);
$this->vultrInstanceStatus = $instanceData['status'] ?? null;
$this->matchedVultrInstance = null;
$this->selectedVultrTokenId = null;
$this->manualVultrInstanceId = null;
$this->vultrNoMatchFound = false;
$this->vultrSearchError = null;
$this->dispatch('success', 'Server successfully linked to Vultr!');
$this->dispatch('close-modal');
$this->dispatch('refreshServerShow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.show');
}
}