fix: cloudflare tunnel configuration, ui, etc

This commit is contained in:
Andras Bacsai
2024-09-23 23:18:23 +02:00
parent 480ae3de8a
commit 688c27c901
10 changed files with 108 additions and 38 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Server; namespace App\Actions\Server;
use App\Events\CloudflareTunnelConfigured;
use App\Models\Server; use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
@@ -40,12 +41,17 @@ class ConfigureCloudflared
instant_remote_process($commands, $server); instant_remote_process($commands, $server);
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e); ray($e);
$server->settings->is_cloudflare_tunnel = false;
$server->settings->save();
throw $e; throw $e;
} finally { } finally {
CloudflareTunnelConfigured::dispatch($server->team_id);
$commands = collect([ $commands = collect([
'rm -fr /tmp/cloudflared', 'rm -fr /tmp/cloudflared',
]); ]);
instant_remote_process($commands, $server); instant_remote_process($commands, $server);
} }
} }
} }

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class CloudflareTunnelConfigured implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public function __construct($teamId = null)
{
if (is_null($teamId)) {
$teamId = auth()->user()->currentTeam()->id ?? null;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
return [
new PrivateChannel("team.{$this->teamId}"),
];
}
}

View File

@@ -33,10 +33,11 @@ class SshMultiplexingHelper
self::validateSshKey($sshKeyLocation); self::validateSshKey($sshKeyLocation);
$checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip}"; $checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) { if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$checkCommand = 'cloudflared access ssh --hostname %h -O check -o ControlPath=' . $muxSocket . ' ' . $server->user . '@' . $server->ip; $checkCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
} }
$checkCommand .= "{$server->user}@{$server->ip}";
$process = Process::run($checkCommand); $process = Process::run($checkCommand);
if ($process->exitCode() !== 0) { if ($process->exitCode() !== 0) {
@@ -54,14 +55,15 @@ class SshMultiplexingHelper
$serverInterval = config('constants.ssh.server_interval'); $serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time'); $muxPersistTime = config('constants.ssh.mux_persist_time');
$establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} " $establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
.self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval)
."{$server->user}@{$server->ip}";
if (data_get($server, 'settings.is_cloudflare_tunnel')) { if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$establishCommand = 'cloudflared access ssh --hostname %h -fNM -o ControlMaster=auto -o ControlPath=' . $muxSocket . ' -o ControlPersist=' . $muxPersistTime . ' ' . self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval) . $server->user . '@' . $server->ip; $establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
} }
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
$establishCommand .= "{$server->user}@{$server->ip}";
$establishProcess = Process::run($establishCommand); $establishProcess = Process::run($establishCommand);
if ($establishProcess->exitCode() !== 0) { if ($establishProcess->exitCode() !== 0) {
@@ -74,10 +76,11 @@ class SshMultiplexingHelper
$sshConfig = self::serverSshConfiguration($server); $sshConfig = self::serverSshConfiguration($server);
$muxSocket = $sshConfig['muxFilename']; $muxSocket = $sshConfig['muxFilename'];
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip}"; $closeCommand = "ssh -O exit -o ControlPath=$muxSocket ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) { if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$closeCommand = 'cloudflared access ssh --hostname %h -O exit -o ControlPath=' . $muxSocket . ' ' . $server->user . '@' . $server->ip; $closeCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
} }
$closeCommand .= "{$server->user}@{$server->ip}";
Process::run($closeCommand); Process::run($closeCommand);
} }
@@ -98,7 +101,7 @@ class SshMultiplexingHelper
} }
if (data_get($server, 'settings.is_cloudflare_tunnel')) { if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$scp_command = 'timeout ' . $timeout . ' cloudflared access ssh --hostname %h -o ControlMaster=auto -o ControlPath=' . $muxSocket . ' -o ControlPersist=' . $muxPersistTime . ' '; $scp_command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
} }
$scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true); $scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
@@ -128,7 +131,7 @@ class SshMultiplexingHelper
} }
if (data_get($server, 'settings.is_cloudflare_tunnel')) { if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$ssh_command = 'timeout ' . $timeout . ' cloudflared access ssh --hostname %h -o ControlMaster=auto -o ControlPath=' . $muxSocket . ' -o ControlPersist=' . $muxPersistTime . ' '; $ssh_command .= "-o ProxyCommand='cloudflared access ssh --hostname %h' ";
} }
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval')); $ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));

View File

@@ -31,13 +31,12 @@ class ConfigureCloudflareTunnels extends Component
{ {
try { try {
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail(); $server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
ConfigureCloudflared::run($server, $this->cloudflare_token); ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true; $server->settings->is_cloudflare_tunnel = true;
$server->ip = $this->ssh_domain; $server->ip = $this->ssh_domain;
$server->save(); $server->save();
$server->settings->save(); $server->settings->save();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); $this->dispatch('warning', 'Cloudflare Tunnels configuration started.');
$this->dispatch('refreshServerShow');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -24,11 +24,16 @@ class Form extends Component
public $timezones; public $timezones;
protected $listeners = [ public function getListeners()
'serverInstalled', {
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'cloudflareTunnelConfigured',
'refreshServerShow' => 'serverInstalled', 'refreshServerShow' => 'serverInstalled',
'revalidate' => '$refresh', 'revalidate' => '$refresh',
]; ];
}
protected $rules = [ protected $rules = [
'server.name' => 'required', 'server.name' => 'required',
@@ -96,6 +101,12 @@ class Form extends Component
} }
} }
public function cloudflareTunnelConfigured()
{
$this->serverInstalled();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
}
public function serverInstalled() public function serverInstalled()
{ {
$this->server->refresh(); $this->server->refresh();

View File

@@ -134,6 +134,9 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
return 'exited'; return 'exited';
} }
$container = format_docker_command_output_to_json($container); $container = format_docker_command_output_to_json($container);
if ($container->isEmpty()) {
return 'exited';
}
if ($all_data) { if ($all_data) {
return $container[0]; return $container[0];
} }

View File

@@ -30,7 +30,7 @@
"service.stop": "This service will be stopped.", "service.stop": "This service will be stopped.",
"resource.docker_cleanup": "Run Docker Cleanup (remove unused images and builder cache).", "resource.docker_cleanup": "Run Docker Cleanup (remove unused images and builder cache).",
"resource.non_persistent": "All non-persistent data will be deleted.", "resource.non_persistent": "All non-persistent data will be deleted.",
"resource.delete_volumes": "All volumes associated with this resource will be permanently deleted.", "resource.delete_volumes": "Permanently delete all volumes associated with this resource.",
"resource.delete_connected_networks": "All non-predefined networks associated with this resource will be permanently deleted.", "resource.delete_connected_networks": "Permanently delete all non-predefined networks associated with this resource.",
"resource.delete_configurations": "All configuration files will be permanently deleted from the server." "resource.delete_configurations": "Permanently delete all configuration files from the server."
} }

View File

@@ -5,7 +5,7 @@
<div class="pb-4">This will stop your containers, delete all related data, etc. Beware! There is no coming back! <div class="pb-4">This will stop your containers, delete all related data, etc. Beware! There is no coming back!
</div> </div>
<x-modal-confirmation title="Confirm Resource Deletion?" buttonTitle="Delete" isErrorButton submitAction="delete" <x-modal-confirmation title="Confirm Resource Deletion?" buttonTitle="Delete" isErrorButton submitAction="delete"
buttonTitle="Delete" :checkboxes="$checkboxes" :actions="['All containers of this resource will be stopped and permanently deleted.']" confirmationText="{{ $resourceName }}" buttonTitle="Delete" :checkboxes="$checkboxes" :actions="['Permanently delete all containers of this resource.']" confirmationText="{{ $resourceName }}"
confirmationLabel="Please confirm the execution of the actions by entering the NAME of the resource below" confirmationLabel="Please confirm the execution of the actions by entering the NAME of the resource below"
shortConfirmationLabel="Resource Name" step3ButtonText="Delete Permanently" /> shortConfirmationLabel="Resource Name" step3ButtonText="Permanently Delete" />
</div> </div>

View File

@@ -1,6 +1,6 @@
<form wire:submit.prevent='submit' class="flex flex-col w-full gap-2"> <form wire:submit.prevent='submit' class="flex flex-col gap-2 w-full">
<x-forms.input id="cloudflare_token" required label="Cloudflare Token" /> <x-forms.input id="cloudflare_token" required label="Cloudflare Token" type="password" />
<x-forms.input id="ssh_domain" label="Configured SSH Domain" required <x-forms.input id="ssh_domain" label="Configured SSH Domain" required
helper="The SSH Domain you configured in Cloudflare. Make sure there is no protocol like http(s):// so you provide a FQDN not a URL." /> helper="The SSH domain you configured in Cloudflare. Make sure there is no protocol like http(s):// so you provide a FQDN not a URL. <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/cloudflare/tunnels/#automated' target='_blank'>Documentation</a>" />
<x-forms.button type="submit" isHighlighted @click="modalOpen=false">Automated Configuration</x-forms.button> <x-forms.button type="submit" isHighlighted @click="modalOpen=false">Continue</x-forms.button>
</form> </form>

View File

@@ -117,37 +117,51 @@
</div> </div>
</div> </div>
<div class="w-96"> <div class="{{ $server->isFunctional() ? 'w-96' : 'w-full' }}">
@if (!$server->isLocalhost()) @if (!$server->isLocalhost())
<x-forms.checkbox instantSave id="server.settings.is_build_server" <x-forms.checkbox instantSave id="server.settings.is_build_server"
label="Use it as a build server?" /> label="Use it as a build server?" />
<div class="flex flex-col gap-2 pt-6"> <div class="flex flex-col gap-2 pt-6">
<div class="flex items-center gap-1"> <div class="flex gap-1 items-center">
<h3 class="text-lg font-semibold">Cloudflare Tunnels</h3> <h3 class="text-lg font-semibold">Cloudflare Tunnels</h3>
<x-helper class="inline-flex" <x-helper class="inline-flex"
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all SSH requests to your server through Cloudflare.<br> You then can close your server's SSH port in the firewall of your hosting provider.<br><span class='dark:text-warning'>If you choose manual configuration, Coolify does not install or set up Cloudflare (cloudflared) on your server.</span>" /> helper="If you are using Cloudflare Tunnels, enable this. It will proxy all SSH requests to your server through Cloudflare.<br> You then can close your server's SSH port in the firewall of your hosting provider.<br><span class='dark:text-warning'>If you choose manual configuration, Coolify does not install or set up Cloudflare (cloudflared) on your server.</span>" />
</div> </div>
@if ($server->settings->is_cloudflare_tunnel) @if ($server->settings->is_cloudflare_tunnel)
<div class="w-64">
<x-forms.checkbox instantSave id="server.settings.is_cloudflare_tunnel" label="Enabled" /> <x-forms.checkbox instantSave id="server.settings.is_cloudflare_tunnel" label="Enabled" />
</div>
@elseif (!$server->isFunctional()) @elseif (!$server->isFunctional())
<div class="p-4 mb-4 text-sm text-yellow-800 bg-yellow-100 rounded-lg dark:bg-yellow-900 dark:text-yellow-300"> <div class="p-4 mb-4 w-full text-sm text-yellow-800 bg-yellow-100 rounded dark:bg-yellow-900 dark:text-yellow-300">
<p>Please select manual cloudflare tunnel configuration (first then hit validate server) or validate the server first and then you can select automatic configuration.</p> <x-slide-over closeWithX fullScreen>
<p class="mt-2">For more information, please read our <a href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/" target="_blank" class="font-medium underline hover:text-yellow-600 dark:hover:text-yellow-200">Documentation</a>.</p> <x-slot:title>Validate & configure</x-slot:title>
<x-slot:content>
<livewire:server.validate-and-install :server="$server" />
</x-slot:content>
To <span class="font-semibold">automatically</span> configure Cloudflare Tunnels, please click
<span @click="slideOverOpen=true"
wire:click.prevent='validateServer' class="underline cursor-pointer">
here.</span> You will need a Cloudflare token and domain.
</x-slide-over>
<br/>
To <span class="font-semibold">manually</span> configure Cloudflare Tunnels, please click <span wire:click="manualCloudflareConfig" class="underline cursor-pointer">here</span>, then you should validate the server.
<br/><br/>
For more information, please read our <a href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/" target="_blank" class="font-medium underline hover:text-yellow-600 dark:hover:text-yellow-200">documentation</a>.
</div> </div>
@endif @endif
@if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional()) @if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional())
<x-modal-input buttonTitle="Automatic Configuration" title="Cloudflare Tunnels" class="w-full"> <x-modal-input buttonTitle="Automated Configuration" title="Cloudflare Tunnels" class="w-full">
<livewire:server.configure-cloudflare-tunnels :server_id="$server->id" /> <livewire:server.configure-cloudflare-tunnels :server_id="$server->id" />
</x-modal-input> </x-modal-input>
@endif @endif
@if (!$server->settings->is_cloudflare_tunnel) @if ($server->isFunctional() &&!$server->settings->is_cloudflare_tunnel)
<x-forms.button wire:click="manualCloudflareConfig" class="w-full"> <div wire:click="manualCloudflareConfig" class="w-full underline cursor-pointer">
I have configured Cloudflare Tunnels manually I have configured Cloudflare Tunnels manually
</x-forms.button> </div>
@endif @endif
</div> </div>
@if (!$server->isBuildServer()) @if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel)
<h3 class="pt-6">Swarm <span class="text-xs text-neutral-500">(experimental)</span></h3> <h3 class="pt-6">Swarm <span class="text-xs text-neutral-500">(experimental)</span></h3>
<div class="pb-4">Read the docs <a class='underline dark:text-white' <div class="pb-4">Read the docs <a class='underline dark:text-white'
href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>. href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>.